对一个class文件的字节码进行逐行的分析是理解class文件结构的最佳方式。但是往往复杂的二进制字节码会让人望而却步,或者只有仔细一点点盯着才能保证不花眼。本文的目的在于尽可能完整地拆解JVM的Class字节码并将其分块分析,最终得到的图解结构希望可以帮助到你。
本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。
根据这个简单的例子来说明
以下的例子作为最简单的一个java程序,通过javac执行编译,javap来查看它的反编译结果,当然我们还会更刨根问底地直接使用二进制编辑器查看class文件的二进制字节排布。
> javap -v Test
Classfile /Users/jinhaoplus/Desktop/Test.class
Last modified 2018-8-12; size 285 bytes
MD5 checksum eac8f02f8ad176b09bfd89cf15e2ed3d
Compiled from "Test.java"
public class top.jinhaoplus.demo.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // top/jinhaoplus/demo/Test.m:I
#3 = Class #17 // top/jinhaoplus/demo/Test
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 top/jinhaoplus/demo/Test
#18 = Utf8 java/lang/Object
{
public int m;
descriptor: I
flags: ACC_PUBLIC
public top.jinhaoplus.demo.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
SourceFile: "Test.java"
图解概况
如上的字节码阅读起来有诸多障碍,因此我们把上面的字节码按照字节码规范定义的class结构分区为不同的颜色块,不同的分区颜色说明这个区域对应着class结构中的不同区域定义,表示一个整体概念的字节码在图中显示为同一行上:
下面的图是class文件结构的思维导图说明,可以跟上述的实际的一个class的分区作简单的对照:
详细解释一下class文件的每个分区
下面详细解释一下class文件的每个分区,括号内的数字表示当前区的占位情况,u是字节的意思,如u4表示占4个字节的空间,对应到图中就是4个方格。
1. magic
magic(u4):魔数,class文件的标识开头。
CAFEBABE
是固定的JVM Class的魔数,也可以认为是众所周知的Java咖啡Logo的由来。
2. version
version:class版本,主次版本合起来即可确定版本号。
- 2.1 minor_version(u2):次版本
- 2.2 major_version(u2):主版本
Class文件的版本为次版本0X0000
、主版本0X0034
,对应的是10进制的52.0。说明此Class是在JDK_VERSION=52.0(JDK1.8)的编译器中生成的,同时又可以被版本在JDK_VERSION=52.0及以上的虚拟机上执行(JVM保持了向下兼容性,但是拒绝执行超过它的版本号的Class字节码)。
3. 常量池:注意是本处的常量池指class字节码中的常量池而非JVM中的常量池(但后者中的数据其实是加载于前者)。
3.1 constant_pool_count
constant_pool_count(u2):常量池大小,定义了常量池中保存的常量个数(准确说常量个数=constant_pool_count-1)。
0X0013
表示constant_pool_count=19,常量池中保存的常量个数=18(编号为#1~#18)。
3.2 constant_pool
constant_pool(constant_pool_count-1个constant_pool_info):实际保存的常量,编号从1开始(将0位留空有特殊考量)。
常量有多种种类,我们这里只提一下我们的Class文件里涉及到的具体的类型。
3.2.1 CONSTANT_Utf8_info
由utf-8编码的二进制串,其字节码格式为
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
其中的tag=0X01
即为CONSTANT_Utf8_info
类型常量的标识。我们Class字节码中的#5
、#6
、#7
、#8
、#9
、#10
、#11
、#12
、#13
、#14
、#17
、#18
都是CONSTANT_Utf8_info
常量,因为它们的首位tag=0X01
(橘色列),通过utf-8解码这些常量指定长度的二进制串可以得出下面的结果,比如#5
号常量length=1(10进制的0X0001
),而bytes为0X6D
,utf-8解码后就是字符串m
,同理可以得到这些二进制串的值(这就是javap反编译出结果的原理,可以参照javap得到的结果对照一下):
#5 m
#6 I
#7 <init>
#8 ()V
#9 Code
#10 LineNumberTable
#11 inc
#12 ()I
#13 SourceFile
#14 Test.java
#17 top/jinhaoplus/demo/Test
#18 java/lang/Object
3.2.2 CONSTANT_Class_info
类常量,其字节码格式为
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
其中的tag=0X07
即为CONSTANT_Class_info
类型常量的标识,index指向了常量池中类的全限定名的索引序号。
我们Class字节码中的#3
、#4
是CONSTANT_Class_info
类型的类常量,它们的首位tag=0X07
(橘色列),通过查找常量池中它们指向的索引序号,我们可以得出这两个类的全限定名:
#3 #17 // top/jinhaoplus/demo/Test
#4 #18 // java/lang/Object
3.2.3 CONSTANT_NameAndType_info
字段或方法的名称和类型常量,其字节码格式为
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X0C
即为CONSTANT_NameAndType_info
类型常量的标识,第一个index指向了字段或方法名称在常量池中的索引序号,第二个index指向了字段或方法的描述符在常量池中的索引序号。
字段的描述符就是简单的字段类型,Class文件中的类型为了节省空间进行了简化:如基本类型int->I
,double->D
,引用类型java/lang/Object -> Ljava/lang/Object
。
我们Class字节码中的#15
、#16
是CONSTANT_NameAndType_info
类型的类常量,它们的首位tag=0X0C
(橘色列),通过查找常量池中它们两个指向的索引序号,我们可以得出常量#15
的名称为#7
号常量即<init>
,类型为#8号
常量即()V
。同理可以得到#16
的意思。
#15 #7:#8 // "<init>":()V
#16 #5:#6 // m:I
3.2.4 CONSTANT_Fieldref_info
字段引用常量,其字节码格式为
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X09
即为CONSTANT_Fieldref_info
类型常量的标识,第一个index指向了声明字段的类或接口的CONSTANT_Class_info
常量在常量池中的索引序号,第二个index指向了字段的名称和类型信息CONSTANT_NameAndType_info
常量在常量池中的索引序号。
我们Class字节码中的#2
是CONSTANT_Fieldref_info
类型的类常量,它们的首位tag=0X09
(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个字段的声明类的是top/jinhaoplus/demo/Test,字段的名称是m,类型是I(即int,Class将类型全称映射到成了单字母)。
#2 #3.#16 // top/jinhaoplus/demo/Test.m:I
3.2.5 CONSTANT_Methodref_info
方法引用常量,其字节码格式为
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X0A
即为CONSTANT_Methodref_info
类型常量的标识,第一个index指向了声明方法的类或接口的CONSTANT_Class_info
常量在常量池中的索引序号,第二个index指向了方法的名称和类型信息CONSTANT_NameAndType_info
常量在常量池中的索引序号。
我们Class字节码中的#1
是CONSTANT_Fieldref_info
类型的类常量,它们的首位tag=0X0A
(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个方法的声明类是java/lang/Object,方法的名称是<init>,类型是()V(即无入参返回void类型的方法)。
#1 #4.#15 // java/lang/Object."<init>":()V
至此我们得到了这个Class中的常量池中全部的常量的含义。这些常量将被下面的其他部分引用到。
4.类信息:
4.1 access_flag
access_flag(u2):说明这个类或接口的访问标志,如private
/public
/interface
/abstract
/annotation
/enum
等,总之是说明了这个类的特征。以不同的特征给出特征位的方式来设置这个u2大小的区域。
如本Class的0X0021
实际代表了特征位信息是0000000000110001,即ACC_SUPER|ACC_PUBLIC,表示它是public的class(ACC_SUPER是JDK1.0.2后的默认设置项)。
4.2 this_class
this_class(u2):说明本类的类索引,0X0003
说明本类索引在常量池中的序号为3,上面常量池的分析可以看到本类的全限定名是top/jinhaoplus/demo/Test
。
4.3 super_class
super_class(u2):说明父类的类索引,0X0004
说明父类索引在常量池中的序号为4,上面常量池的分析可以看到父类的全限定名是java/lang/Object
(这也就是所有Java类的父类都是Object的原因,即使没有明确写出来编译后的Class文件中也会将这个父类声明定义出来)。
4.4 interface_info
4.4.1 interface_count(u2):说明实现的接口数量,0X0000
说明本类没有实现接口,因此不再有接下来的interface信息。
4.4.2 interface(interface_count个u2):说明接口的类索引。
5.字段信息
5.1 field_count(u2):字段数量
5.2 field_info(field_count个field_info):字段信息,字段表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
5.2.1 access_flag(u2)用以记录字段的特征。
比如private
/public
/protected
/static
/final
/volatile
,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中的0X0001
(橙色列)实际代表了特征位信息是0000000000000001,即字段的特征是ACC_PUBLIC(public字段)。
5.2.2 name_index(u2)是字段名在常量池中的索引序号。
我们的Class中的0X0005
(蓝色列)指向的常量池中的#5
号常量即m
。
5.2.3 descriptor_index(u2)是字段描述符在常量池中的索引序号。
我们的Class中的0X0006
(青色列)指向的常量池中的#6
号常量即I
。
5.2.4 字段的属性表是本字段的属性表:
- 5.2.4.1 attributes_count(u2):字段属性表的属性数量,我们的Class中的
0X0000
表示本字段无额外的属性表信息。 - 5.2.4.1 attributes(attributes_count个attribute_info):字段属性表的属性信息,字段属性有自己定义的结构,字段中主要使用的属性包括
ConstantValue
(final修饰的常量值作为字段的值)、Depreciated
(@Depreciated修饰的字段表示弃用)、Signature
(泛型参数记录的泛型签名信息,否则编译后擦除类型就无法溯源了)等,他们都有各自定义的结构。
6.方法信息
6.1 method_count
method_count(u2):方法数量
我们的Class这个区的0X0002
表示这个类有两个方法。
6.2 method_info
method_info(method_count个method_info):方法信息,方法表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
6.2.1 access_flag(u2)用以记录方法的特征。
比如private
/public
/protected
/static
/final
/synchronized
,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中两个方法的这个区域的0X0001
(橙色列)实际代表了特征位信息是0000000000000001
,即它们的特征都是ACC_PUBLIC
(public方法)。
6.2.2 name_index(u2)是方法名在常量池中的索引序号。
我们的Class中,method_#1
的0X0005
(蓝色列)指向的常量池中的#7
号常量即<init>
,而。method_#2
的0X000B
(蓝色列)指向的常量池中的#11
号常量即inc
。
6.2.3 descriptor_index(u2)是方法描述符在常量池中的索引序号。
我们的Class中,method_#1
的0X0008
(青色列)指向的常量池中的#8
号常量即()V
,而。method_#2
的0X000C
(青色列)指向的常量池中的#12
号常量即()I
。
6.2.4 字段的属性表是该方法的属性表:
- 6.2.4.1 attributes_count(u2):方法属性表的属性数量。
- 6.2.4.2 attributes(attributes_count个attribute_info):方法属性表的属性信息,方法属性有自己定义的结构,方法中主要使用的属性包括最重要的
Code
(方法的字节码指令,没有方法执行体的接口和抽象类是没有这个属性的)、Exceptions
(声明方法抛出的异常)、Depreciated
(@Depreciated修饰的方法表示弃用)、Signature
(泛型参数记录的泛型签名信息)等,他们都有各自定义的结构。这里我们具体来看一下最重要的Code
属性。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
- a. attribute_name_index(u2):属性名在常量池中的索引序号,
Code
属性最终找到的常量肯定是Code
。 - b. attribute_length(u4):该属性的长度。
- c. max_stack(u2):该方法的操作数栈最大深度。
- d. max_locals(u2):该方法的局部变量表的大小。
- e. code_length(u4):字节码指令的大小
- f. code(exception_table_length个u1):字节码。
- g. exception_table_length(u2):异常表大小。
- h. exception_table(exception_table_length个exception_info):异常表大小。
- i. attributes_count(u2):方法属性表的大小。
- j. attributes(attribute_count个attribute_info):方法属性表。
接下来用我们Class的两个方法来详细说明Code
属性:
method_#1
方法:
- i. attributes_count = 1
- ii. attributes:
- a. attribute_name_index:常量
0X0009
即Code
。 - b. attribute_length:29(
0X0000001D
),即下一位起后的29u都是这个属性。 - c. max_stack:1(
0X0001
)。 - d. max_locals:1(
0X0001
)。 - e. code_length:5(
0X00000005
)。 - f. code:0X2AB70001B1。(字节码指令的具体含义鉴于与class结构是相对独立的主题不再详述,后续会再单独深入介绍)
- g. exception_table_length:0(
OX0000
)。 - h. exception_table:无。
- i. attributes_count:1(
0X0001
)。 - j. attributes:
- attribute_name_index:LineNumberTable(
0X000A
),说明这是一个用于记录源码行号和字节码行号映射的属性表。 - attribute_length:6(
0X00000006
). -
attribute:LineNumberTable属性表的内部结构:
- line_number_table_length:1(
0X0001
)。 - line_number_index:0:3(
0X00000003
)。
- line_number_table_length:1(
method_#2
方法的分析方式如上类似不再赘述。
Class字节码的结构为什么这么设计
乍一看来上面的结构让人很难快速理解,但是如果理解JVM的字节码结构的设计目的就可以加深理解了。
JVM的字节码结构其实是一种由字节码堆砌的表型结构,充分定义占位的结构可以无歧义地将它想要表达的原义还原回去。作为二进制结构主要的表达方式,只要定义好占位情况,表型结构可以通过层层嵌套定义来实现更为复杂的结构、并且可以实现良好的拓展。
比如上面的介绍的方法信息通过方法数量定义了这个表的大小,而每个表entry内部可以再有自己的定义,比如方法信息中还可以包含属性表(即在方法表内部再嵌套一层表),比如这里定义了Code属性表,而Code属性表自身又有良好的表结构定义,这个表内部除了一些一维的字段(比如index、count等不能拓展的字段)外,还有额外的exception_table,但是因为有exception_table_length的表大小限制就可以无歧义地还原回去,此外还有attribute_info,但是因为有attribute_count的表大小限制也可以无歧义地还原回去。用下面的思维导图我们可以直观地看出来这种良好的定义,图中加入了每个一维节点的占位大小:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。