《深入理解Java虚拟机》学习笔记(四)(类文件结构)

图0 Java字节码排列方式

魔数与Class文件的版本

  • 魔数(Magic Number)
    • 每个Class文件的头4个字节
    • 唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件
    • 值为:0xCAFEBABE
图1 一个class文件的字节码(前8个字节)
  • 次版本号

    • 第5和第6个字节
  • 主版本号

    • 第7和第8个字节

Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。 这个容量计数是从1而不是0开始的。

  • 第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。


    图2 常量池入口数据u2,0x16表示有21项常量(1~21)
  • Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、 字段表集合、 方法表集合等的容量计数都与一般习惯相同,是从0开始的。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)

  • 字面量
    • 比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。
  • 符号引用
    • 属于编译原理方面的概念,包括了下面三类常量
      • 类和接口的全限定名(Fully Qualified Name)
      • 字段的名称和描述符(Descriptor)
      • 方法的名称和描述符

常量池中每一项常量都是一个表,在JDK 1.7时共有14种结构各不相同的表结构数据,表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。

图3 常量池的项目类型

UTF-8缩略编码与普通UTF-8编码的区别:从'\u0001'到'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从'\u0080'到'\u07ff'之间的所有字符的缩略编码用两个字节表示从'\u0800'到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

图4 访问标志

类索引、 父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系

  • 类索引
    • 用于确定这个类的全限定名
  • 父类索引
    • 用于确定这个类的父类的全限定名
    • 由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0
  • 接口索引集合
    • 用来描述这个类实现了哪些接口
    • 被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
    • 入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。

类索引、 父类索引和接口索引集合都按顺序排列在访问标志之后

字段表集合

用于描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

图5 字段表结构
  • access_flags:与类中的access_flags项目是非常类似的,都是一个u2的数据类型
  • name_index:字段的简单名称
  • descriptor_index: 字段和方法的描述符
描述符

用来描述字段的数据类型、 方法的参数列表(包括数量、 类型以及顺序)和返回值

  • 基本数据类型(byte、 char、 double、 float、 int、 long、 short、 boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示
图6 描述符标识字符含义
  • 对于数组类型,每一维度将使用一个前置的“[”字符来描述
    • 如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述。参数列表按照参数的严格顺序放在一组小括号“()”之内。 如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,方法intindexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,inttargetCount,int fromIndex)的描述符为“([CII[CIII)I”。

字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、 名称索引(name_index)、 描述符索引(descriptor_index)、 属性表集合(attributes)几项

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面

如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名

  • 特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合
    • 因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。

属性表集合

  • 在Class文件、 字段表、 方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

  • 不要求各个属性表具有严格顺序

  • Code属性

    • Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内

    • Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性


      图7 Code属性表的结构
    • attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称

    • attribute_length:指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节

    • max_stack:代表了操作数栈(Operand Stacks)深度的最大值。 在方法执行的任意时刻,操作数栈都不会超过这个深度。 虚拟机运行的时候需要根据这个值来分配栈帧(StackFrame)中的操作栈深度。

    • max_locals:代表了局部变量表所需的存储空间。 在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。 对于byte、 char、 float、 int、 short、 boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。 方法参数(包括实例方法中的隐藏参数“this”)、 显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、 方法体中定义的局部变量都需要使用局部变量表来存放。 另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

    • code_length:字节码长度

    • code:用来存储Java源程序编译后生成的字节码指令


      图8 方法的字节码指令

      Args_size为1,表示Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。 这个处理只对实例方法有效,如果方法声明为static,那Args_size就不会等于1而是等于0了。

      在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的

  • Exceptions属性
    是在方法表中与Code属性平级的一项属性,作用是列举出方法中可能抛出的受查异常

  • LineNumberTable属性
    用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

  • LocalVariableTable属性
    用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系

  • SourceFile属性
    用于记录生成这个Class文件的源码文件名称。

  • ConstantValue属性
    通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
    对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。
    目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

  • InnerClasses属性
    用于记录内部类与宿主类之间的关联。

  • Deprecated及Synthetic属性
    Deprecated属性用于表示某个类、 字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。
    Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的

  • StackMapTable属性
    会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用

    • 包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。
  • Signature属性
    任何类、 接口、 初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。

  • BootstrapMethods属性
    用于保存invokedynamic指令引用的引导方法限定符。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容