类文件的结构
Class类文件是以8个字节为单位的二进制流,由魔数、版本号、常量池、类信息、父类信息、接口表、字段表、方法表和属性表组成。下图清晰的展示了Class类文件的结构。
Class类文件示例
预先准备好一段简单的Java代码和编译好的二进制字节流。
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种数据结构,下图右边部分。
上图左边部分已根据定义好的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命令让我们更直观的感受下常量池。
类信息和父类信息
类信息由类访问符
、类名索引
和父类名索引
组成,他们各占2个字节。
查看偏移地址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个字段。
上图中已经标示了字节流中的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个方法。
上图中已经标示了字节流中的2个方法(^角开头的数字)。现以第一个方法为例来进行讲解。
- 偏移地址0x000000dE-0x000000dF的值为0x0001,定义了方法的访问符,查找方法访问符表可知,该方法由public修饰。
- 偏移地址0x000000e0-0x000000e1的值为11(=0x000b),定义了方法名称索引,表示指向第11个常量项(<init> ,即构造器)。
- 偏移地址0x000000e2-0x000000e3的值为12(=0x000c),定义了方法描述符索引,表示指向第12个常量项(()V,即void)
- 偏移地址0x000000e4-0x000000fA定义了该方法的属性,属性表稍后讲解。
最后通过javap命令让我们更直观的感受下方法表。
属性表
ConstantValue属性
一般定义在字段表中。下图展示了ConstantValue属性的数据结构。
在字段表中说过,偏移地址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属性的数据结构。
在方法表中说过,偏移地址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虚拟机原理图解