类文件的结构

Class类文件是以8个字节为单位的二进制流,由魔数、版本号、常量池、类信息、父类信息、接口表、字段表、方法表和属性表组成。下图清晰的展示了Class类文件的结构。

java 文件偏移量 java内存偏移量_数据结构

Class类文件示例

预先准备好一段简单的Java代码和编译好的二进制字节流。

java 文件偏移量 java内存偏移量_字段_02

Class类文件是如何组成的

接下来会用上述简单的Java代码为示例来讲解Class类文件是如何组成的?

魔数和版本号

魔数是用来检查字节流是不是Class类文件,占4个字节。Class类文件的魔数是cafebabe
查看偏移地址0x00000000-0x00000003,得到魔数是0xcafebabe,可以得出该字节流是一个Class类文件。

版本号是指Jdk的版本号,占4个字节。前2个字节是次版本号,后2个字节是主版本号。
查看偏移地址0x00000004-0x00000007,得到次版本号是0(=0x0000),主版本号是52(=0x0034),也就是版本号是52.0,对应的是jdk8。

常量池

常量个数指常量池中有多少个常量,占2个字节。
查看偏移地址0x00000008-0x00000009,得到该常量池中一共有22(=0x17-1)个常量。减1的原因是常量是从1开始计数的。

常量指名称(类名、字段名、方法名等)或描述符的字面量或符号引用。

  • 字面量可以理解为不经过翻译的字符串。比如Java文件中的类名Hello字符串;比如字段名a、b、m字符串;比如方法名inc字符串;比如返回类型int字符串;
  • 符号引用可以理解为由标签和索引(指向字面量或标签)组成。比如类和接口的全限定名;比如字段的名称和描述符;比如方法的名称和描述符。

接下来我们看下这22个常量在字节流中是怎样体现的?查找常量就像密码学里根据密码表找到对应的密码一样。首先准备好定义好的17种数据结构,为了节省篇幅,这17种数据结构请读者参考《深入理解Java虚拟机第3版》常量池一节的表6.6。这里仅列出4种数据结构,下图右边部分。

java 文件偏移量 java内存偏移量_字段_03


上图左边部分已根据定义好的17种数据结构列出了Hello.class的22个常量项(#号开头的数字),现以第一个常量项为例来进行讲解。

  • 第1号常量项的偏移地址0x0000000A的值为10(=0x0a),对应的是CONSTANT_Methodref_info标签。根据该标签的数据结构可得,一共占5个字节,第1个字节就是前面说的标签;第2-3个字节表示该方法属于哪个类,这里指向的是第4号常量项;第4-5字节表示该方法的名称和描述符(指返回类型),这里指向的是第19号常量项。
  • 第4号常量项的偏移地址0x00000017的值为7(=0x07),对应的是CONSTANT_Class_info标签。根据该标签的数据结构可得,一共占3个字节,第1个字节就是前面说的标签;第2-3个字节表示该方法所在类的全限定名,这里指向的是第22号常量项。
  • 第22号常量项的偏移地址0x0000009F的值为1(=0x01),对应的是CONSTANT_Utf8_info标签。根据该标签的数据结构可得,一共占6个字节,第1个字节就是前面说的标签;第2-3个字节表示方法名的长度,这里的值为16个字节,接下来的16个字节清晰的展示了类的全限定名(java/lang/Object)。
  • 第19号常量项的偏移地址0x0000008D的值为12(=0x0c),对应的是CONSTANT_NameAndType_info标签。根据该标签的数据结构可得,一共占5个字节,第1个字节就是前面说的标签;第2-3个字节表示该方法的名称,这里指向的是第11号常量项;第4-5字节表示该方法的描述符,这里指向的是第12号常量项。
  • 第11号常量项的偏移地址0x0000003F的值为1(=0x01),对应的是CONSTANT_Utf8_info标签。与第22号常量项类似,最后清晰的展示了方法名称(<init> ,即构造器)。
  • 第12号常量项的偏移地址0x00000048的值为1(=0x01),对应的是CONSTANT_Utf8_info标签。与第22号常量项类似,最后清晰的展示了方法描述符(()V ,即void)。

最后通过javap命令让我们更直观的感受下常量池。

java 文件偏移量 java内存偏移量_字段_04

类信息和父类信息

类信息由类访问符类名索引父类名索引组成,他们各占2个字节。

java 文件偏移量 java内存偏移量_数据结构_05


查看偏移地址0x000000b2-0x000000b3,得到值为0x0021(=0x0001+0x0020),查表可得类访问符为public super。

查看偏移地址0x000000b4-0x000000b5,得到值为3(=0x0003),表示指向第3号常量项(Hello)。

查看偏移地址0x000000b6-0x000000b7,得到值为4(=0x0004),表示指向第4号常量项(java/lang/Object)。

接口表

接口数量占用2个字节。查看偏移地址0x000000b8-0x000000b9,得到值为0,也就是说该类没有实现相关接口。
接口表描述了接口相关信息。

字段表

字段数量占用2个字节。查看偏移地址0x000000bA-0x000000bB,得到值为3(=0x0003),也就是说该类中有3个字段。
字段表描述了字段相关信息。

接下来我们根据字段表结构来读下Hello.class字节流中的3个字段。

java 文件偏移量 java内存偏移量_java 文件偏移量_06


上图中已经标示了字节流中的3个字段(^角开头的数字)。现以第一个字段为例来进行讲解。

  • 偏移地址0x000000bC-0x000000bD的值为0x001a(=0x0002+0x0008+0x0010),定义了字段的访问符,查找字段访问符表可知,该字段由private static final修饰。
  • 偏移地址0x000000bE-0x000000bF的值为5(=0x0005),定义了字段名称索引,表示指向第5个常量项(a)
  • 偏移地址0x000000c0-0x000000c1的值为6(=0x0006),定义了字段描述符索引,表示指向第6个常量项(I,即int)
  • 偏移地址0x000000c2-0x000000cB定义了该字段的属性,属性表稍后讲解。
方法表

方法数量占用2个字节。查看偏移地址0x000000dC-0x000000dD,得到值为2(=0x0002),也就是说该类中有2个方法。
方法表描述了方法相关信息。

接下来我们根据方法表结构来读下Hello.class字节流中的2个方法。

java 文件偏移量 java内存偏移量_字段_07


上图中已经标示了字节流中的2个方法(^角开头的数字)。现以第一个方法为例来进行讲解。

  • 偏移地址0x000000dE-0x000000dF的值为0x0001,定义了方法的访问符,查找方法访问符表可知,该方法由public修饰。
  • 偏移地址0x000000e0-0x000000e1的值为11(=0x000b),定义了方法名称索引,表示指向第11个常量项(<init> ,即构造器)。
  • 偏移地址0x000000e2-0x000000e3的值为12(=0x000c),定义了方法描述符索引,表示指向第12个常量项(()V,即void)
  • 偏移地址0x000000e4-0x000000fA定义了该方法的属性,属性表稍后讲解。

最后通过javap命令让我们更直观的感受下方法表。

java 文件偏移量 java内存偏移量_数据结构_08

属性表

ConstantValue属性一般定义在字段表中。下图展示了ConstantValue属性的数据结构。

java 文件偏移量 java内存偏移量_字段_09


在字段表中说过,偏移地址0x000000c2-0x000000cB定义了字段a的属性。

  • 偏移地址0x000000c2-0x000000c3的值为1(=0x0001),定义了该字段属性的个数。
  • 偏移地址0x000000c4-0x000000c5的值为7(=0x0007),定义了该字段属性名称的索引,表示指向第7个常量项(ConstantValue)。
  • 偏移地址0x000000c6-0x000000c9的值为2(=0x0002),定义了该属性的长度,表示该属性一共占用2个字节。
  • 偏移地址0x000000ca-0x000000cb的值为8(=0x0008),定义了该属性的值,表示指向第8个常量项(属性值为10)。

Code属性一般定义在字段表中。下图展示了Code属性的数据结构。

java 文件偏移量 java内存偏移量_字段_10


在方法表中说过,偏移地址0x000000e4-0x000000fA定义了<init>方法的属性。

  • 偏移地址0x000000e4-0x000000e5的值为1(=0x0001),定义了该方法属性的个数。
  • 偏移地址0x000000e6-0x000000e7的值为13(=0x000d),定义了该方法属性名称的索引,表示指向第13个常量项(Code)。
  • 偏移地址0x000000e8-0x000000eB的值为29(=0x001d),定义了该属性的长度,表示该属性一共占用29个字节。
  • 偏移地址0x000000eC-0x000000eD的值为1(=0x0001),定义了该方法操作数栈深度。
  • 偏移地址0x000000eE-0x000000eF的值为1(=0x0001),定义了该方法局部变量表大小。
  • 偏移地址0x000000f0-0x000000f3的值为5(=0x0005),定义了方法体的长度。
  • 偏移地址0x000000f4-0x000000f8的值为0x2ab70001b1,定义了方法体的指令,这些指令都可以在虚拟机字节码指令表中查到。
    1)读入2a,查表得0x2a对应的指令为aload_0,表示将第0个变量槽中reference类型(指this)推送到操作数栈顶。
    2)读入b7,查表得0xb7对应的指令为invokespecial,invokespecial用于调用实例构造器、private方法或者它的父类的方法。读入0001,指向第1个常量项,表示invokespecial调用的是实例构造器。
    3)读入b1,查表得0xb1对应的指令为return,表示方法正常结束。
  • 偏移地址0x000000f9-0x0000108,依次定义了异常表、LineNumberTable属性,这里不作进一步讲解。

为了节省篇幅,属性表中只介绍ConstantValue属性和Code属性两种比较重要的属性,其他的属性,比如Exceptions属性、SourceFile属性等,有兴趣的朋友可以阅读《深入理解Java虚拟机第3版》属性表一节。

Reference

深入理解Java虚拟机第3版
Java虚拟机原理图解