public class Test {
public int a = 3;
static Integer si = 6;
String s = "Hello World!";
public static void main (String arg[]){
Test test = new Test();
test.a = 8;
si = 9;
}
public void test(){
this.a = a;
}
}
javac
将 Test.java 编译成 Test.classjavap -verbose Test
获取 Test.class 对应的字节码Classfile /Users/zhishengjie/workspace/learning/vm/Test.class
Last modified 2021-7-13; size 627 bytes
MD5 checksum 7122e264980126ec0b9f4c7b76021ffe
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#26 // java/lang/Object."":()V
#2 = Fieldref #5.#27 // Test.a:I
#3 = String #28 // Hello World!
#4 = Fieldref #5.#29 // Test.s:Ljava/lang/String;
#5 = Class #30 // Test
#6 = Methodref #5.#26 // Test."":()V
#7 = Methodref #31.#32 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#8 = Fieldref #5.#33 // Test.si:Ljava/lang/Integer;
#9 = Class #34 // java/lang/Object
#10 = Utf8 a
#11 = Utf8 I
#12 = Utf8 si
#13 = Utf8 Ljava/lang/Integer;
#14 = Utf8 s
#15 = Utf8 Ljava/lang/String;
#16 = Utf8
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 test
#23 = Utf8
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #16:#17 // "":()V
#27 = NameAndType #10:#11 // a:I
#28 = Utf8 Hello World!
#29 = NameAndType #14:#15 // s:Ljava/lang/String;
#30 = Utf8 Test
#31 = Class #35 // java/lang/Integer
#32 = NameAndType #36:#37 // valueOf:(I)Ljava/lang/Integer;
#33 = NameAndType #12:#13 // si:Ljava/lang/Integer;
#34 = Utf8 java/lang/Object
#35 = Utf8 java/lang/Integer
#36 = Utf8 valueOf
#37 = Utf8 (I)Ljava/lang/Integer;
{
public int a;
descriptor: I
flags: ACC_PUBLIC
static java.lang.Integer si;
descriptor: Ljava/lang/Integer;
flags: ACC_STATIC
java.lang.String s;
descriptor: Ljava/lang/String;
flags:
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_3
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String Hello World!
12: putfield #4 // Field s:Ljava/lang/String;
15: return
LineNumberTable:
line 1: 0
line 2: 4
line 4: 9
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class Test
3: dup
4: invokespecial #6 // Method "":()V
7: astore_1
8: aload_1
9: bipush 8
11: putfield #2 // Field a:I
14: bipush 9
16: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: putstatic #8 // Field si:Ljava/lang/Integer;
22: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 14
line 10: 22
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field a:I
5: putfield #2 // Field a:I
8: return
LineNumberTable:
line 13: 0
line 14: 8
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 6
2: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #8 // Field si:Ljava/lang/Integer;
8: return
LineNumberTable:
line 3: 0
}
SourceFile: "Test.java"
值得注意的是:使用 javap -verbose 命令分析一个字节码文件时,将会分析字节码文件的魔数、版本号、常量池、类信息、类的构造函数、类中所包含的方法信息以及类(成员)变量信息。需要注意的是,每一次执行 javap 命令所输出的信息内容一定是相同的,但是信息的先后顺序则不保证完全一致,例如,常量池中的元素编号每次都不保证相同。
所有 .class
字节码文件的开始四个字节都是 魔数 ,并且其值一定是 0xCAFEBABE ,这里的 CAFEBABE 是指十六进制数值,并不是字符串 “CAFEBABE”,如果开始的4个字节不是 0xCAFEBABE ,则JVM将会认为该文件不是 .class
字节码文件,并拒绝解析。
读者可以根据左侧的行号进行定位,图中选中的部分就是 魔数
根据字节码文件规范,魔数之后的四个字节为版本信息,前两个字节表示 major version ,即主版本号;后面两个字节为 minor version ,即次版本号。
这里版本号的值为 0x00000034 对应的十进制数是 52 。目前已知发布的version:1.1(45)、1.2(46)、1.3(47)、1.4(48)、1.5(49)、1.6(50)、1.7(51)、1.8(52)。据此可以知道,该 class 文件是 JDK1.8 编译的。
常量池是 .class 字节码文件中非常重要和核心的内容,一个java类中的绝大多数的信息都是有常量池描述的,尤其是java类中定义的变量和方法,都是常量池保存的。注意,对 JVM 所有研究的人,可能都知道 JVM 的内存模型中,有一块就是常量池,JVM堆栈的常量池就是用于保存每一个 Java 类所对应的常量池的信息的,一个java应用程序中所包含的所有Java类的常量池组成了jvm堆区中大的常量池。
Java类所对应的常量池主要由常量池数量和常量池数组两部分组成,常量池数量紧跟在次版本号的后面,占2字节。常量池数组则紧随在常量池数量之后。
常量池数组,顾名思义,就是一个类似数组的结构。这个数组固化在字节码文件中,由多个元素组成。但是每一个元素的第一个数据都是u1类型,该字节是标志位,占一个字节。JVM解析常量池时,根据u1类型来获取该元素的具体类型。常量池的组成结构如下图所示。
常量池元素中的不同元素结构与类型都是不同的,正因为如此,JVM只能定义有限的元素类型,并针对有限的类型进行专门解析。JVM一共定义了11中常量,如下图所示。
可以看到,类的方法信息、接口和继承信息、属性信息都是定义在NamedAndType_Info中的,关于该结构,下文会详细讲解。
常量池数组中的每一种元素的内容都是复合数据结构的,下面分别给出jvm所定义的常量池中的每一种元素的具体结构。
举例说明:
该截图中 tag 值为 1 ,通过核对 JVM 常量池元素一览表 可知,tag 位标识为 1 对应的常量池元素为 CONSTANT_Utf8_info(含义:UTF-8编码字符串)。其组成结构分为3部分,分别是:tag、length、bytes,其中tag和length的长度分别是u1、u2(u后面的数字是几,就代表占几个字节),即分别占1字节和2字节。而 bytes 则是字符串的具体内容,其长度是 length 字节。在字节码文件中,该常量池元素最终所占的字节数是:1 + 2 + length 。其他类型的常量池元素的组成结构类似,这里不一一分析,下文在实例解析常量池组成结构时会在吃住个进行详细解析
常量池中的元素类型只可能是常量池元素结构中的那12种类型,而且每个元素结构的的第1个字节永远都是tag,可通过tag去对应常量池元素结构表中的数据结构,查看每个结构的含义。据此,我们来推断一下常量池中的第一个元素。
第一个常量池元素在魔数(4字节)、major version(2字节)、minor version(2字节)、常量池数量(2字节)之后,也就是从第11个字节开始,就是常量池中的的第一个常量池数组。
下图中第11位是 0x0A 转为十进制位为 10,参照常量池元素结构 图可知数据结构是CONSTANT_methodref_info。
关于类型u1,u2,u4,u8的解释:
Class文件格式采用一种类似C语言结构伪结构存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型以u1,u2,u4,u8来分别代表一个字节、2个字节,4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字条串值。
由此可以推断出第一个元素占 1+2+2 = 5 个字节,进而绘制了如下表格
序号 | 类型 | 结构 | 对应16进制 | 转化为10进制 | 十进制含义 |
---|---|---|---|---|---|
1 | u1 | tag | 0x0A | 10 | |
2 | u2 | index | 0x0009 | 9 | 指向常量池的第9个元素; |
3 | u2 | index | 0x001A | 26 | 指向常量池的第26个元素; |
有了上文的基础,我们就可以对常量池进行切割,方便我们定位元素的位置,常量池的数量是 0x0026 等于 38,真实的常量个数是 38-1 = 37个,之所以比字节码文件中的少一个,是因为JVM会保留地0号常量池位置,一次仅从第1个开始计算。见下图:
开始分析表格中的序号2:0x09 转化为十进制为9,指向常量池中的第9个元素,第9个元素是0x070022,tag为0x07的元素结构为
index 代表全限定类名的常量项索引,占用两个字节为 0x0022 ,转化为十进制为 34,由此可知,第九位元素又指向了常量池的第34位元素,第34位元素为0x01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,正好每两个字节代表一个字符,对应的字符串是java/lang/Object,如下图所示:
序号3的原理一样,所以不再赘述。
分析完字节码的二进制后,我们再看一下通过命令 javap -verbose Test
获取 Test.class 对应的字节码。
不用多说,是不是有点意思。
查看类成员变量a的信息,见下图:
上图中所选中的8个字节,一共包含两个常量池的元素信息,因为其tag位都是1,所以这两个常量池元素的类型都是字符串。
第一个字符串常量的length是1,对应的十六进制是0x61,正好对应UTF-8编码的字符a。第二个字符串常量的length也是1,对应的十六进制是0x49,正好对应UTF-8编码的字符I。在JVM规范中,若变量的类型是I,则表示该变量的实际类型是int。这与上文变量a的定义一致。
接着看类变量si的定义,如下图。
上图中算选中的27个字节,一共描述了两个常量池元素,这两个常量池元素的类型也都是字符串。第一个字符串的length为2,其值是0x7369,对应的UTF-8编码的字符串si。第二个字符串的length为0x13,即19,其值是0x 4C 6A 61 76 61 2F 6C 61 6E 67 2F 49 6E 74 65 67 65 72 3E,这一串值是ASCII字符,每2个十六进制数对应一个ASCII字符,这些数字连接起来就对应一个字符串,所对应的字符是Ljava/lang/Integer。
接下来看类成员变量s的定义,如下图。
上图中选中的25个字节,一共描述了两个常量池的元素,这两个常量池的元素类型也都是字符串。第一个字符串的lentgth为1,其值是0x73,对应UTF-8编码的字符s。第二个字符床的length为0x12,即18,其值是0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B,对应的UTF-8编码的字符串Ljava/lang/String。这两个常量池元素合起来描述了Test类中的s字符串变量。
在上面我们讲了如何分析常量池的元素,但是一个.class 字节码文件中共有10个组成部分。下面将分析.class字节码文件的后续组成部分。
在字节码文件中,常量池数组之后就是access_flags结构,该结构类型是u2,占两个字节。access_flags代表访问标志位,该标志用于标注类或者接口层次的访问信息,如当前Class是类还是接口,是否定义为public类型,是否定义为abstract类型等。从下图可以看到access_flags为0x0021。
根据JVM的access_flags的可选值如下表所示。
因为access_flag = 0x0021 可以拆解成 0x0020 和 0x0001,由此可以知道该类的访问标识既包含ACC_PUBLIC,也包含ACC_SUPER。自JDK1.2以后,类被编译出来的invokespecial字节码指令是否允许使用的选项都是真的,因此access_flags的值都会带有ACC_SUPER标识位。
access_flags访问标识之后就是this_class结构,该结构的类型是u2,占两个字节。this_class 为0x0005记录当前类的全限定类名,其值指向常量池中对应的索引值。
将十六进制0x0005转化为十进制为5,所以指向常量池中的第5个元素。上文我们已经分析过二进制流了,为了方便直观,我们直接看字节码,可以知道第五个元素是Test,因为没有建包,所以全限定类名就是Test。
在this_class之后就是super_class结构,该结构的类型是u2,占2字节。super_class记录当前类的父类全限定类名,其值指向常量池中对应的索引值。将十六进制的0x0009转化为十进制为9,指向常量池的第9个元素。
只要明白其原理,分析起来就特别的简单。由于Test.class并没有显式继承任何基类,因此编译时便默认继承java.lang.object。这与字节码中的super_class值是一致的。
super_class访问标识符之后就是interfaces_count结构,该结构类型是u2,占2字节。interface_count结构记录当前类所实现的接口数量。
因为没有继承任何类,所以是0
interfaces 表示接口索引集合,是一组u2类型数据集合,该结构描述当前类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。因为interfaces_count值为0,因此字节码文件中并没有intefaces信息。
访问标识符之后就fields_count,该结构类型是u2,占2字节,该值记录当前类中所定义的变量总数量,包括类成员变量和类变量(即静态变量)。
Test.class类一共包含3个变量,上图显示也是3个,正好相互验证。
在字节码文件中,fields_count之后的是field结构,该结构长度不确定,不同类型所长长度是不同的。fields记录类中所定义的各个变量的详细信息,包括变量名,变量类型、访问标、属性等。
要分析fields机构信息,首先需要清楚该结构的数据组成格式,其结构如下表所示
说明:
access_flags,表示变量的访问标识,该值是可选的,由JVM规范规定。
name_index,表示变量的简单名称引用,占2字节,其值指向常量池的索引。
descriptor_index,表示变量的类型信息引用,占2字节,其值指向常量池的索引。
Fields结构实际上是一个数组,数组中的每一个元素的结构都如上表所示,即每一个元素都包含访问标识、米昂成索引、描述信息索引、属性数量和属性信息。其中,如果属性数量为0,则没有属性信息,由于访问标识、名称、描述信息、属性数量的字节长度是确定的,因此JVM可以在解析过程中计算出fields结构所占的全部字节数。
变量的access_flags,有下表所示的可选项。
其中,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED这3个标识只能选择一个,结构中的字段必须有ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志,class文件对此并无规定,这些都是java语言所要求的。
在fields_count之后就是第一个fields,通过上文的fields组成结构表可知,fields一共有5个不部分,第一部分就是access_flags,,在二进制流中是0x0001,参考** access_flags可选项表**可知代表ACC_PUBLIC。第二部分是名称索引是0x000A转化为十进制是10,依此类推类型索引是0x000B为11
通过字节码文件查看常量池中第10和第11位元素的内容
因为在0x000B之后的2字节0x0000说明a的属性数量是0,因为没有属性,所以字段描述结构中的最后元素attributes也就不存在了。
这里的I是什么意思呢?在JVM规范中,每个变量/字段都有描述信息,描述信息主要描述字段的数据类型、方法的参数的参数列表(参数类型,参数数量,参数顺序)和返回值。根据描述规则,基本数据类型和代表无返回值的void类型都用一个大写的字符表示,而对象类型则用字符L加对象的全限定类名表示。为了压缩字节码文件的体积(字节码文件最终也占用服务器硬盘资源和内存资源),对于基本数据类型,JVM都仅使用一个大写字母标识。如下图所示是各个基本数据类型所对应的标识符。
对于数组类型,每一维将使用一个前置的“[”字符俩藐视,如“int[]”将被记录为“[I,String[][]”将被记录为“[[Ljava/lang/String;”。
用于描述方法时,按照先参数列表,后返回值顺序描述,参数列表按照参数的严格顺序放在一组“()”之内,如方法“String getAll(int id, String name)” 的描述为“(I,Ljava.lang/String;)Ljava/lang/String;”
变量a总共占8个字节,如下图所示:
通过JVM规范,我们对si(棕色)和s(紫色)所在的二进制流分别标记了出来,因为其attrbutes_count都是0,所以都是占8个字节。变量si的access_flags是8,表示这是一个带有static修饰符的变量,而变量s的access_flags是0,表示该变量没有任何访问修饰符。
两个变量名称分别引用常量池中12和13号元素,对照上文的字节码文件可知,其变量类型分别是Ljava/lang/Integer和Ljava/lang/String。一次也可知,对于引用类型的变量字节码文件描述其变量类型的格式是“L+类全限定名”。
在字节码文件中,紧跟在变量的结构fileds后面的是methods_count结构,该结构类型是u2,占2字节。该结构描述类中一共有多少个方法。
由上图可知,其值是4,即Test类中一共有4个方法。可能很多人对此会有疑惑,在Test源程序中明明只有两个方法,为什么字节码文件中却像是了4个呢?这是因为在编译期间编译器会自动为一个类增加void()这样一个方法,其方法名就是“”,返回值为void。该方法的作用主要是执行类的初始化,源程序中所有static 类型的变量都在这个方法中完成初始化,全部被static{}所包围的程序都会在这个方法中执行。同时,在源代码中,并没有为Test类定义构造函数,因此编译器会自动为该类增加一个默认的构造函数。因此,字节码文件会显示Test类中一共包含4个方法。
在methods_count后面的就是methods结构,这是一个数组,每一个方法的全部细节都包含在里面,包括代码指令。
要分析methods结构信息,首先需要清楚该结构的数组组成格式,其格式如图4.7(方法表结构和字段结构一样)所示。
由上表可知,方法各个数据项的含义非常相似,仅在访问标识位和属性表集合的可选项上有略微不同。这些字段的含义与上文给出的fields结构字段含义基本相同,因此这里不作具体说明。
其中,JVM规范为access_flags规定了一组可选项值,如下表所示
由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_STRICTFP标志和ACC_ABSTRACT标志。
上面讲解了方法描述的信息结构,下面来实际看看Test.class字节码文件的第一个方法究竟是如何描述的。紧跟在methods_count后面的就是一个方法的信息,如下图所示。
按照fields的机构组成格式,前2字节描述access_flags,即访问标识,其值为0x0001,对照上文所给出的方法访问标识可选项值的表可知,该值标识的方式的修饰符是public。
接下里的2字节藐视name_index,该字段描述的是方法名,其值指向常量池中对应的元素编号。由此上图可知。其值是0x0010,指向常量池中的第16号元素,参照根据字节码文件可知,常量池的第16号元素是
当java类中定义了构造方法,或其他非static类成员变量被赋了初始值,编译器便会生成
接下来是descriptor_index,该字段描述的是方法的入参和出参,其指向常量池中对应的元素编号,有上图可知,其值是0x0011,指向常量池中第17号元素,根据字节码文件可知,常量池中的第17号元素是()V,这表示当前方法没有入参(因为是空括号),并且方法的返回类型是void(V代表viod)。这里要注意,按照JVM的规范,描述符对入参将严格按照源程序中所定义的参数列表顺序,从左到右依次放入“()”内,如方法“String getAll(int id, String name)”的描述符为“(I,Ljava/lang/String)Ljava/lang/String”。由此规律可以推断出,”()V”字节码描述的方法是void
接下来2字节描述的方法所包含的属性总数量attributes_count,由上图可知值为0x0001,表示当前方法一共包含1个属性。
access_flags为 0x0008,表示AC_STATIC,这是一个static类型的静态方法。
name_index为 0x0017,该字段描述的方法名,指向常量池中的对应元素编号,为
descriptor_index为0x0011,该字段描述的是方法的入参和出参,其指向常量池中对应的元素编号,有上图可知,其值是0x0011,指向常量池中第17号元素,根据字节码文件可知,常量池中的第17号元素是()V,这表示当前方法没有入参(因为是空括号),并且方法的返回类型是void(V代表viod)。
attributes_count为0x0001,描述的方法所包含的属性总数量,可知,该方法有一个属性。
https://download.csdn.net/download/qq_39774931/20333578
参考文献:《揭秘Java虚拟机-JVM设计原理与实现》