前言
JVM上不仅仅能够运行Java语言,还能运行其他多种语言,比如说Clojure、Groovy、JRuby、Scala等等。其实Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。正如下图所示

Class类文件的结构
简述:Class文件是一组以8位字节为基础单位的二进制流,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种微结构中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。
直接讲Class类的文件结构感觉有些抽象,这边我是用一个自己写的Java小程序来帮助逐步理解。用WinHex对编译好的Class文件进行分析。
1 | public class HelloWorld { |
将编译好的Class文件放入WinHex中

魔数与Class文件的版本
魔数:每个Class文件的头四个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。它的值为0xCAFEBABY(咖啡宝贝)。
Class文件的版本:紧接着魔数的四个字节。其中第五第六个字节是次版本号,第七第八个字节是主版本号。这边是0x00000034,次版本号为0,主版本号为十进制的52,也就是JDK1.8对应的版本号,能向低版本兼容。
常量池
紧接着Class版本号的就是常量池,常量池可以理解为Class文件之中的资源仓库。它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池的常量数据是不确定的,所以在常量之前需要放置一项u2类型的数据,代表常量池容量计数值,注意,它的计数是从1开始的而不是0。这边是0x0024,也就是有35个常量。
常量池中主要存放两大类常量:字面量和符号引用。
①字面量:接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
②符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。 Java代码在经过javac命令编译之后,生成的Class文件中并不会保存各个方法、字段的最终内存布局信息,必须等到虚拟机运行时,从常量池中获得对应的符号引用,再通过这些符号引用经过类创建或运行时解析、翻译等才能得到具体的内存地址。
常量池中共有14中类型的常量,每一个常量(项)都有自己的表结构,但这14种表的有一个共同特点:表开始的第一位是一个u1类型的标志位tag,代表这个常量属于哪种常量类型。常量池中的常量类型见下表。
手动的去一个一个分析常量池中的每一个常量非常麻烦,这边我就分析第一个当个例子。
这边的第9和10位代表常量池容量,换算成十进制是36,由于下标是从1开始,所以就只有1-35共35个常量。接下去的0x0A通过查常量池的数据类型表可以得知代表了类方法的符号引用,也就是CONSTANT_Methodref_info,通过查结构总表查看它的结构。
很清楚的看到它的结构为一个u1类型的tag和两个u2类型,两个u2用十六进制代表了0x0006和0x0016,分别代表了指向常量表中其他常量的两个索引,分别是第6个常量和第22个常量。
其实Java给了一个解析常量池的工具javap,和之前的性能分析工具一样,放在JDK的bin目录下,可以通过javap -verbose class文件名来访问。访问结果如下,可以清楚的看到35个常量的情况。其中第一个常量分别指向6和22个常量,和我们之前分析的一样。另外,还出现了a I
这种常量,这个在后面的字段表、方法表、属性表中会用到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 >$ javap -verbose /c/Users/hqf/Desktop/HelloWorld.class
>Classfile /C:/Users/hqf/Desktop/HelloWorld.class
Last modified 2019-8-12; size 572 bytes
MD5 checksum 85882996f632cd60591fe0bf0fc8cad3
Compiled from "HelloWorld.java"
>public class com.JVMTest.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
>Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // HelloWorld
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #28 // com/JVMTest/HelloWorld
#6 = Class #29 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/JVMTest/HelloWorld;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 HelloWorld.java
#22 = NameAndType #9:#10 // "<init>":()V
#23 = Class #30 // java/lang/System
#24 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#25 = Utf8 HelloWorld
#26 = Class #33 // java/io/PrintStream
#27 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#28 = Utf8 com/JVMTest/HelloWorld
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
>{
public com.JVMTest.HelloWorld();
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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/JVMTest/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String HelloWorld
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
>}
>SourceFile: "HelloWorld.java"这边再随意分析一个常见的常量类型,字符串类型。
这边的tag为01,代表CONSTANT_Utf8_info这个常量,其中0x0013代表字符串的长度,也就是16,我们向后数16个字节,刚好到蓝色最后端的位置,换算过来的字符串为java/io/PrintStream
访问标志
常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。
在我们这个Class为0x0021这两位,查表得是0x0001和0x0020运算的结果(0x0001|0x0020=0x0021)。所以访问标志位为ACC_PUBLIC和ACC_SUPER两个为真,其余标志位为假。
类索引、父索引与接口索引集合
在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。由于Java中是单继承,所以父类索引只有一个;但Java类可以实现多个接口,所以接口索引是一个集合。
类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用”/”代替”.”。比如Object的全限定名是java.lang.Object。父类索引确定这个类的父类的全限定名,除了Object之外,所有的类都有父类,所以除了Object之外所有类的父类索引都不为0。接口索引集合存储了implements语句后面按照从左到右的顺序的接口。
具体来说就是
其类索引、父类索引、接口索引分别是0x0005、0x0006、0x0000,代表了类索引为5,父类索引为6,接口索引的集合大小为0。其中类索引和父类索引指向的是CONSTANT_Class_info的类描述常量符,再通过这个常量的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。我们再把用javap获得的常量表拿下来分析一下。
类索引为5,也就是指向的类名为com/JVMTest/HelloWorld,其中的“.”用”/“代替了。
父类索引为6,也就是java/lang/Object,也就是Object这个父类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 #1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // HelloWorld
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #28 // com/JVMTest/HelloWorld
#6 = Class #29 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/JVMTest/HelloWorld;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 HelloWorld.java
#22 = NameAndType #9:#10 // "<init>":()V
#23 = Class #30 // java/lang/System
#24 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#25 = Utf8 HelloWorld
#26 = Class #33 // java/io/PrintStream
#27 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#28 = Utf8 com/JVMTest/HelloWorld
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
字段表集合
字段表用于描述接口或类中声明的变量。字段又分为类字段(静态属性)和实例字段(对象属性)。保存一个字段需要保存的信息有以下这些:
字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。
这些信息中,各个修饰符可以用布尔值表示。而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。字段表格式是这样子的
其中字段修饰符放在access_flags中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下
access_flags后面name_index和descriptor_index,前者是字段的简单名称的常量池索引,后者是字段和方法的描述符的常量池索引。
字段的简单名称:是指没有类型和参数修饰的方法或者字段名称。比如说代码中的”a”、“main”。
字段和方法的描述符:
字段的描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的Void类型都用一个大写的字符来表示。如下所示
另外,对于数组类型,每一维将使用一个前置的“[”字符来描述,如定义一个java.lang.String[][]类型的二维数组,将记录为[[Ljava/lang/String,一个double数组double[]将标记为[D。
方法的描述符:按照先参数列表,后返回值的顺序描述。参数列表按照参数的严格顺序放在一组小括号”()”之内,比如方法void inc()的描述符为”()V”
下面就对之前的那个例子进行分析
首先第一个u2代表容量计数器fields_count,值为0x0001代表字段表中有一个字段。第二个u2代表access_flags,值为0x0002,查询相应的表得到ACC_PRIVATE,字段是private类型的。第三个u2代表name_index,值为0x0007,查询该索引对应的内容为a,表示字段名称为a。第四个u2代表descriptor_index,值为0x0008,查询内容为I,是一个int类型的数据。所以综合得到字段:private int a;
方法表集合
方法表集合和字段表集合比较类似,方法表的结构与字段表格式相同
其中方法访问标志位如下
对Class文件进行分析
其中第一位u2表示methods_count,也就是方法的数量,其值为2,也就是有两个方法,一个是编译器添加的实例构造器
和源码中的main()方法。 第一个方法:第二位u2表示access_flags,也就是访问标志位,值为0x0001,代表是public方法。第三位表示名称索引,值为0x0009,值为9,对应的常量为“
”。第四位表示描述符索引,值为0x000A,查找得到“()V”。第五位表示属性表计数器,为0x0001,表示有一个属性,其索引的值为第六个u2,也就是0x000B,属性的值为Code。 整个第一个方法区域为,后面多出来的一长串就是方法体。
所以第一个方法是 public
()V 第二个方法:先给出第二个方法的前面几位
分析一下,0x0009表示(0x0001|0x0008)=0x0009,也就是标志位是public和static,0x0010代表方法名索引,值为16,找到”main”。接下去0x0011表示描述符索引,值为17,找到“([Ljava/lang/String;)V”,后面的位数与之前第一个方法的类似。
所以第二个方法是public static main ([Ljava/lang/String;)V
属性表集合
属性表在之前已经出现过了多次,之前出现的Code就是一个比较常见的属性,下面主要介绍Code属性。
其中attribute_name_index和attribute_length前面已经介绍过了。
max_stack代表了操作数栈的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机执行时需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需要的存储空间。在这里,max_locals的单位是slot。方法参数(包括隐式参数this)、显式异常处理器的参数(try-catch块中catch块中定义的异常)以及方法体中定义的局部变量都需要局部变量表来存放。需要注意的是,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给各个变量使用,然后计算max_locals的大小。
code_length和code用来存储字节码指令。Java的字节码指令的长度都是一个字节,即最多可以有256个指令,实际上一共有大约200条指令。对于字节码指令这里不过多介绍。
exception_table_length和exception_table分别是指异常表长度,和异常表集合。
attributes_count和 attributes是Code属性中的属性表集合。


















