所构造的Java测试类如下:
public class Test { public int a = 3; static Integer si = 6; String s = "Hello,World!"; public static void main(String[] args) { Test test = new Test(); test.a = 8; si = 9; } private void test (){ this.a = a; } }
使用IDEA进行编译后产生class文件,使用 javap -v Test.class
分析。
λ javap -v Test.class Classfile /E:/IDEA-Project/JVM/out/production/JVM/Test.class Last modified 2021-7-3; size 757 bytes MD5 checksum 96cabb2b8e3e791a984abfcd1676ab88 Compiled from "Test.java" public class Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #9.#31 // java/lang/Object."<init>":()V #2 = Fieldref #5.#32 // Test.a:I #3 = String #33 // Hello,World! #4 = Fieldref #5.#34 // Test.s:Ljava/lang/String; #5 = Class #35 // Test #6 = Methodref #5.#31 // Test."<init>":()V #7 = Methodref #36.#37 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #8 = Fieldref #5.#38 // Test.si:Ljava/lang/Integer; #9 = Class #39 // 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 <init> #17 = Utf8 ()V #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 LocalVariableTable #21 = Utf8 this #22 = Utf8 LTest; #23 = Utf8 main #24 = Utf8 ([Ljava/lang/String;)V #25 = Utf8 args #26 = Utf8 [Ljava/lang/String; #27 = Utf8 test #28 = Utf8 <clinit> #29 = Utf8 SourceFile #30 = Utf8 Test.java #31 = NameAndType #16:#17 // "<init>":()V #32 = NameAndType #10:#11 // a:I #33 = Utf8 Hello,World! #34 = NameAndType #14:#15 // s:Ljava/lang/String; #35 = Utf8 Test #36 = Class #40 // java/lang/Integer #37 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer; #38 = NameAndType #12:#13 // si:Ljava/lang/Integer; #39 = Utf8 java/lang/Object #40 = Utf8 java/lang/Integer #41 = Utf8 valueOf #42 = 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."<init>":()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 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LTest; 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 "<init>":()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 LocalVariableTable: Start Length Slot Name Signature 0 23 0 args [Ljava/lang/String; 8 15 1 test LTest; 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"
在IDEA中安装插件 jclasslib
,并选择编译产生的class文件,查看。
使用HxD工具打开class文件。
所有.class字节码文件的开始的4个字节代表魔数,并且其值一定为 0xCAFE BABE
(咖啡宝贝 =。=)
如果开始的4个字节不是CAFE BABE, 则 JVM 将会认为该文件不是 .class 字节码文件,会拒绝解析。
在魔数后面紧跟的4个字节分别为 次版本号
和 主版本号
;
可以看到这里的次版本号为0,主版本号为 00 34 即对应十进制的52。
JDK version | major主版本号 | minor 次版本号 |
---|---|---|
1.0 | 44 | 3 |
1.1 | 45 | 3 |
1.2 | 46 | 0 |
1.3 | 47 | 0 |
1.4 | 48 | 0 |
1.5 | 49 | 0 |
1.6 | 50 | 0 |
1.7 | 51 | 0 |
1.8 | 52 | 0 |
可以利用 jclasslib插件查看:
后面2个字节代表常量池常量数,00 2B 对应十进制的 43;但是根据 javap 反编译后的文件( 上面的1.2 节 ),由于索引是从0开始的,但所有的字节码文件0位索引代表null,不显示出来,因此能看到的常量池数是 43-1 = 42个:
Constant pool: #1 = Methodref #9.#31 // java/lang/Object."<init>":()V #2 = Fieldref #5.#32 // Test.a:I #3 = String #33 // Hello,World! #4 = Fieldref #5.#34 // Test.s:Ljava/lang/String; #5 = Class #35 // Test #6 = Methodref #5.#31 // Test."<init>":()V #7 = Methodref #36.#37 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #8 = Fieldref #5.#38 // Test.si:Ljava/lang/Integer; #9 = Class #39 // 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 <init> #17 = Utf8 ()V #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 LocalVariableTable #21 = Utf8 this #22 = Utf8 LTest; #23 = Utf8 main #24 = Utf8 ([Ljava/lang/String;)V #25 = Utf8 args #26 = Utf8 [Ljava/lang/String; #27 = Utf8 test #28 = Utf8 <clinit> #29 = Utf8 SourceFile #30 = Utf8 Test.java #31 = NameAndType #16:#17 // "<init>":()V #32 = NameAndType #10:#11 // a:I #33 = Utf8 Hello,World! #34 = NameAndType #14:#15 // s:Ljava/lang/String; #35 = Utf8 Test #36 = Class #40 // java/lang/Integer #37 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer; #38 = NameAndType #12:#13 // si:Ljava/lang/Integer; #39 = Utf8 java/lang/Object #40 = Utf8 java/lang/Integer #41 = Utf8 valueOf #42 = Utf8 (I)Ljava/lang/Integer;
java类所对应的常量池主要由常量池数量和常量池数组两部分组成, 常量池数组紧跟在常量池数量后面;
常量池数组中的不同的元素的类型,结构都是不同的,长度也是不同的;
但是每一种元素的第一个数据都是一个u1类型 ( 1字节 ),该字节是标志位;JVM 解析常量池时依靠一个标志位来获取该元素的具体类型;
常量池的每一种元素类型都是复合的数据结构类型,下面分别给出 JVM 所定义的常量池中每一种元素的具体结构。
即对应
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V ... #9 = Class #39 // java/lang/Object ... #16 = Utf8 <init> #17 = Utf8 ()V #31 = NameAndType #16:#17 // "<init>":()V
常量池的第一个元素的方法引用常量,引用的方法来自java/lang/Object这个类(#9 第9号常量池元素), 方法名和类型为(#31 第31号常量池元素), 可以看到 方法名和类型 指向#31号常量池元素,但是该元素又指向#16和#17, 同理可得#16项常量池元素为方法名,#17号常量池元素为类型; 即方法名为,类型为 ()V , 其中()为入参,括号内为空表示没有入参,V代表方法的返回类型为void。
即对应:
第二个元素是 字段符号引用常量:
其指向的所属类名是 #5:Test类 , 指向的字段描述符(字段名字和类型)为 #32:a和I, 其中a为字段名,I为类型代表整数类型;
当最后一个常量池元素结束时,class字节码文件对应到:
在字节码文件中,常量数组之后紧跟着的是 access_flags
结构,该结构的类型是u2(2字节), access_flags
代表访问标志位,该标志用于标注类或接口层次的访问信息,例如 描述当前类是类还是接口,是否定义为public类型,是否定义为abstract类型等。
由于 Test.class
中的 access_flags=0x0021
,因此该类的访问表标识既包含了 ACC_PUBLIC
也包含了 ACC_SPUER
;
在字节码文件中, 紧跟 access_flags
访问标识之后的是 this_class
结构, 该结构是u2类型(2字节);
this_class
记录当前类的全限定名(包名+类名), 其值指向常量池中对应的索引值;
指向#5号常量池
#5 = Class #35 // Test
在字节码文件中, 紧跟 this_class
访问标识之后的是 super_class
结构, 该结构是u2类型(2字节);
this_class
记录当前类父类的全限定名(包名+类名), 其值指向常量池中对应的索引值;
指向#9号常量池
#9 = Class #39 // java/lang/Object
在字节码文件中, 紧跟 super_class
访问标识之后的是 interfaces_count
结构, 该结构是u2类型(2字节);
interfaces_count
记录当前类所实现的接口数量;
在字节码文件中, 紧跟 interfaces_count
访问标识之后的是 interfaces[interfaces_count]
结构, 表示一组接口索引集合,是一组u2类型数据的集合(即每个接口使用2字节来索引常量池元素的标号),这些被实现的接口将按 implements
语句(如果该类为接口,则为 extend
语句) 后的接口顺序从左至右排列在接口的索引集合中;
由于Test.class的interfaces_count值为0,因此字节码文件中并没有interfaces信息
在字节码文件中, 紧跟 interfaces
访问标识之后的是 fields_count
结构, 该结构是u2类型(占2个字节)。 该值记录当前类中所定义的变量总数,包括类成员变量和类变量(即静态变量);
即对应:
在字节码文件中, 紧跟 fields_count
字段信息之后的是 fields_info_fields
结构, 该结构长度不确定,不用的变量类型所占的长度是不同的。
fileds
记录类中所定义的各个变量的详细信息,包括变量名,变量类型,访问标识和属性等;
1. fields的组成结构格式
2. **变量access_flags结构**
根据字节码文件,可以看到第一个域变量为 public, 变量名指向常量池#10,变量类型等描述指向常量池#11,并且属性数量为0, 因此并没有属性信息字段;
对应可以看:
需要注意的是: 类型的标识字符与类型的关系如下:
同理可得其他2个变量,可以分析得到:
对应到
在字节码文件中, 紧跟字段信息之后的是方法信息;首先的methods_count
结构,该结构是u2类型(占2字节)。
该结构描述类中的一共包含多少方法;
由上图分析可得,类中共有4个方法。但是类中明明就只有2个方法, 一个test方法,一个main方法。
需要注意的是:
1. 类中虽然没有写构造函数,但是编译器会自动为该类添加一个默认的构造函数 2. 编译期间,编译器会自动为该类增加 一个 `void <clinit>()` 方法, 其方法名就是 **<clinit>** ,返回值为 void; 3. <clinit>方法作用主要是执行类的初始化,源程序中的所有**static类型**的变量都会在这个方法中完成初始化,全部被 **static {}**
语句块所包围的程序都在这个方法中执行;
紧跟着methods_count
结构的是 methods结构
, 这是一个数组, 每一个方法的全部细节都包含在里面, 包括代码指令。
0x0001:access_flags, 说明该方法的访问属性为 public
0x0010:name_index, 该字段描述的是方法名, 其值指向常量池对应的元素编号, 对应#16
#16 = Utf8 <init>
**0x0011:descriptor_index,该字段描述的是方法的入参和出参信息, 其值指向常量池对应的元素编号,对应#17 **
#17 = Utf8 ()V ;; 说明入参为空,返回类型为void
0x0001:attributes_count, 该字段描述方法的属性数量,其值说明该方法有一个属性, 见下面的分析