目录
背景
在这个心想事不成的寒假里,还是学习宋红康的JVM视频,整理并发布笔记吧
概述
1.8版本的JVM官网:https://docs.oracle.com/javase/specs/jvms/se8/html/,字节码文件格式章节为https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
字节码文件的跨平台性
java是跨平台的语言,一次编写(译)到处运行,也就是java代码被编译成字节码后,就可以无须再次编译就能在不同平台上运行了,因此统一而强大的字节码文件结构,是jvm的基石与桥梁。
由于oracle有jvm规范,而所有的jvm都会遵守这一规范,因此我们的字节码文件可以在所有的jvm上运行
java的前端编译器
jvm结构可以用下图表示,前端编译器负责把java源码编译成字节码文件;后端编译器是执行引擎中的JIT编译器,用来把热点代码编译成汇编或机器指令
透过字节码指令看代码细节
示例1
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y); // true
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false
}
}
对应字节码指令如下
0 iconst_5
1 invokestatic #2 <java/lang/Integer.valueOf>
4 astore_1
5 iconst_5
6 istore_2
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue>
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf>
37 astore 4
39 getstatic #3 <java/lang/System.out>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println>
56 sipush 128
59 invokestatic #2 <java/lang/Integer.valueOf>
62 astore 5
64 sipush 128
67 invokestatic #2 <java/lang/Integer.valueOf>
70 astore 6
72 getstatic #3 <java/lang/System.out>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println>
90 return
首先
Integer x = 5;
int y = 5;
这三行对应字节码0~6行
0 iconst_5
1 invokestatic #2 <java/lang/Integer.valueOf>
4 astore_1
5 iconst_5
6 istore_2
加载一个整型常量5,调用Integer的静态方法valueOf,存到局部变量表的1号位(0号位存放形参args)。然后再加载一个整型常量5,存到局部变量表2号位
而后
System.out.println(x == y);
对应字节码7~23行
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue>
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
先调用System的静态类out,然后加载局部变量表1号位(x),调用Integer静态方法intValue,就是把value取出来。然后加载局部变量表2号位(y),判断是否不相等,不相等的话加载整型常量1,否则跳到22行,加载整型常量0。然后到第23行,调用PrintStream静态方法println。
再看
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); // true
这三行对应字节码
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf>
37 astore 4
39 getstatic #3 <java/lang/System.out>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println>
跟上面类似,把两个整型10入栈,调用Integer的静态方法valueOf()装箱,分别存到局部变量表3和4的位置,也就是i1和i2。其中关键方法Integer.valueOf()代码如下
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache.low为-128,IntegerCache.high为127,所以数值在[-128, 127]区间内的Integer对象用同一个IntegerCache对象,而超出此范围就直接new一个新的Integer对象,这也就是为何第45行字节码if_acmpne的结果是相等,因为这俩就是同一个对象。而
Integer i3 = 128;
Integer i4 = 128;
i3和i4的值范围超过了127,所以i3和i4用的是俩Integer对象,所以比较结果自然为false
示例2
public class StringTest {
public static void main(String[] args) {
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1); // false
}
}
对应字节码指令
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <hello>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <world>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 ldc #10 <helloworld>
37 astore_2
38 getstatic #11 <java/lang/System.out>
41 aload_1
42 aload_2
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #12 <java/io/PrintStream.println>
54 return
其中
String str = new String("hello") + new String("world");
对应字节码0~31行
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <hello>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <world>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
上来先new一个StringBuilder,然后将其复制一份(dup),再调用其构造方法。然后到第7行new一个String对象,同样复制一份,为其加载字面量hello。而后调用String的构造方法,再调用StringBuilder的父类方法append,将当前对象String最佳道StringBiilder中。相似的步骤在19~28行再次进行,然后调用StringBuilder的父类方法toString(),此方法代码如下
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
显然是new了一个String对象,那么自然和下面的str1是俩对象了
String str1 = "helloworld";
示例3
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father`s x = " + x);
}
}
class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
public void print() {
System.out.println("Son`s x = " + x);
}
}
/*
Son`s x = 0
Son`s x = 30
20
*/
首先看main函数的字节码,因为那里是程序的入口
0 new #2 <Son>
3 dup
4 invokespecial #3 <Son.<init>>
7 astore_1
8 getstatic #4 <java/lang/System.out>
11 aload_1
12 getfield #5 <Father.x>
15 invokevirtual #6 <java/io/PrintStream.println>
18 return
因为我们是new的Son对象,所以上来先创建Son对象,复制一份,然后调用Son的构造方法。因此我们要看看Son的构造方法对应的字节码
0 aload_0
1 invokespecial #1 <Father.<init>>
4 aload_0
5 bipush 30
7 putfield #2 <Son.x>
10 aload_0
11 invokevirtual #3 <Son.print>
14 aload_0
15 bipush 40
17 putfield #2 <Son.x>
20 return
先加载当前对象到局部变量表0号位,然后调用父类的初始化方法,父类初始化方法的字节码如下
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 bipush 10
7 putfield #2 <Father.x>
10 aload_0
11 invokevirtual #3 <Father.print>
14 aload_0
15 bipush 20
17 putfield #2 <Father.x>
20 return
也是一样,加载当前对象(这里是那个son)到局部变量表0号位,然后调用父类Object的构造方法,而后再加载当前对象,整型10入栈,存到Father的x中(注意不是Son的x),然后调用当前对象son的print()方法。此处虽然字节码显示的是<Father.print>,但由于子类有一个同名方法,所以invokevirtual是调用的子类重载的print(),而此时子类变量仅仅被初始化了零值,还没有被显式初始化,所以变量x值为0,故而第一次输出为Son`s x = 0。最后把整型20入栈,赋给父类变量x
父类构造方法执行完后,回到子类构造方法,执行第4行,加载当前son对象,然后把整型30入栈,赋给Son对象的域x,这就是显式赋值。而后调用覆写的方法print(),此时输出的x自然是30。然后再把整型40入栈赋给Son对象的变量x。
回到main方法字节码第12行,获取的是父类的x(Father.x),所以输出为20,因为变量是没有覆写这一说的。这也就是所谓的父类引用指向子类对象,此引用的对象是子类类型的,但引用却是父类,所以它只能看到父类的变量,除非把它强制类型转换成子类
class文件
class文件对应唯一一个类或接口的定义信息,但不一定以磁盘文件的形式存在,也可以通过网络传输的形式加载,本质是一组二进制流。它没有分隔符,所以其中的数据项,无论是字节顺序还是数量、哪个字节代表什么含义、长度如何都不能改变。
字节码文件采用一种类似于C结构体的方式进行数据存储,这种结构中只有俩数据类型:无符号数和表。
无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1个、2个、4个和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成的字符串值。
表有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的用"_info"结尾,用于描述有层次关系的复合结构数据。整个字节码文件本质上就是一张表,由于表没有固定长度,所以前面通常加上数字以表长度
字节码指令
jvm的指令由一个字节长度、代表某种特定操作含义的操作码及跟随其后的零个或多个操作数构成,由java源代码生成
查看方式
有idea集成的jclasslib或单独的jclasslib客户端、一个一个二进制的看(HEX-Editor、BinaryViewer)、javap
class文件结构
以java代码
package test;
public class Demo {
private int num;
private int add() {
num = num + 2;
return num;
}
}
的字节码为例,学习class文件的结构
先贴一下字节码指令的格式
魔数:class文件标志
魔数在字节码文件中的位置如下图所示
可见头四个字节CA FE BA BE就是魔数,用来标明这是一个标准的字节码文件。换言之,标准的字节码文件都是以CA FE BA BE开头的
class文件版本号
版本号在字节码文件中的位置如下图所示
魔数后面四个字节表示字节码文件的版本号,头两个字节00 00表示编译副版本,后俩字节00 34为主版本
如上图所示,主版本16进制34表示十进制的52,再加上副版本00,两者一起表示jdk1.8。
常量池:存放所有常量
字节码版本类型再往后两个字节,就是常量池计数器,存放有多少个常量
也就是说这里的00 16就是常量池长度,即十进制的22,但它的索引范围却是[1, 21],把0号位空出来了,这是为了满足后面某些特殊情况下,常量池中被索引的数据要表达“不引用任何一个常量池项”的含义,那么就用索引0来表示这种数据。
随后21个字节就都表示常量池表项了,存放编译时生成的各种字面量和符号引用,这部分内容在类加载时放入方法区(jdk8开始就是元空间了)的运行时常量池。
字面量包括文本字符串("szc"等)和final常量值(final int a = 10中的10);符号引用包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符。
全限定名形式类似于com/szc/MyClass;,名称就是简单名称,例如add,描述符包括字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。
数据类型描述符如下图所示
关于符号引用,有一些补充说明:虚拟机在加载字节码文件时才会进行动态链接,也就是说,字节码文件中不会保存各个方法或字段的内存信息。当虚拟机运行时,需要从常量池中获取对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中,这一过程就是动态链接。
符号引用和直接引用的区别与关联:
符号引用用一组符号来描述所引用的目标,其与虚拟机的内存布局无关;直接引用则可以是直接指向目标的指针、相对偏移量、能间接定位到目标的句柄。直接引用和虚拟机的内存布局是相关的。
下面就开始解析字节码文件,此例中,常量表项范围从0A到74,21个,如下图所示
显然不是直接算出来的,而是一个个数出来的。另外,常量池中的常量类型如下两图所示,一会儿解析常量表项时要经常用到
后三个(15、16、18)从jdk7开始引入,以支持java的动态语言特性。
现在就开始数常量表,一开始的0A就表示类中方法的符号引用,如表中所示,0A中包含一个u1和两个u2字段,所以它总共占5个字节,也就是后面的00 04 00 00也属于0A
随后的09,同样的道理,还是5个字节,也就是09到13属于第二项。数到第五项01时,发现是字符串,第二部分的两个字节表示长度,此处为3,那么就要在03后面数三个字节,6E~6D,这三个表示此字符串的字节,所以第五项从01到6D。如此循环往复,当数完21项后,正好到74截止
上图就是所有21个常量,常量间用红色竖线分割。
第一项0A 00 04 00 12,是0A类型,也就是方法的符号引用,后四个字节分别是两个字节的类描述符和两个字节的名字与类型描述符。前者值为04,表示常量表中第4项,也就是07 00 15,而07正是类或接口的符号引用,后面俩字节指向第21项,也就是01 00 09 74 .... 6F,01表示字符串类型,后面俩字节表示字符串的字节长度,此处为9,所以后面9个字节就表示字符串的ascii码,如下图所示
也就是java/lang/Object,此为类的名字,也就是第一项的方法所在类名;回到第一项,名字与类型描述符值为12,也就是十进制的18,指向第18项0C 00 07 00 08,这一项的tag为0C,正是字段或方法的符号引用,后面分别是俩字节的名字和俩字节的类型,为第7和第8项,01 00 06 3C 69 6E和01 00 03 28 29 56 ,对应字符串<init>和()V,也就是void类型的构造方法,第一项就此结束。综上所述,第一项指的就是Object类的构造方法
第二项09 00 03 00 13,09表示这一项是字段的符号引用,后面也是俩字节的名字和俩字节的类型,指向第3和第19项,分别是07 00 14和01 00 0F ... 65。前者07表示这是一个类的符号引用,后面俩字节指向它的名字,是第20项,也就是01 00 09 ... 6F,对应字符串test/Demo,说明此类名字是test/Demo;而第19项0C 00 05 00 06,0C表示这是字段或方法的符号引用,后面是俩字节的名字和俩字节的类型,指向第5和第6项,分别是01 00 03 6E 75 6D和01 00 01 49,对应字符串num和I,也就是int型的num,再结合指向它的0C,说明这个整型num是个字段。综上,第二项表示Demo类的字段num
剩下的第9~17和第20项分别表示字符串Code、LineNumberTable、LocalVariableTable、this等字符串,这都是一些独立的关键字或类全限定名等。
这样,常量池表就解析完了,我们再看看ide里jclasslib中的常量池,会更加明白
再看那张常量类型表:
会发现这十四种常量结构的共同点是:开始的第一个字节表示标志位,即哪种常量结构。而第一个CONSTANT_utf8_Iinfo是一种utf-8编码的字符串信息,后面跟的是字符串的字节长度和字节内容
常量池可以理解为字节码文件中的资源仓库,是字节码文件中与其他项关联最多的结构,也是占用字节码文件空间最大的项目之一
常量池存在的意义:
字节码文件中不会保存各个方法、字段的内存布局信息,所以这些字段的符号引用不经过运行期链接是不能得到真是的内存地址的,也因此无法被虚拟机直接使用。而虚拟机运行时,可以从常量池得到所有的符号引用,再在类创建时或运行时解析翻译到具体的内存地址中。
访问标识
常量池之后紧跟着的就是访问标识,两字节表示,用于识别一些类或接口的访问信息,具体见下图
会发现类或接口的访问权限只有public,说明它只标识公有的字节码文件类或接口。如果某内部类或字节码对应的类或接口不是public的,那此项就关闭。
对于我们的例子,访问标识为00 21,也就是20 + 1,对应ACC_PUBLIC | ACC_SUPER,后者默认使能,前者正对应我们Demo类的访问类型public。此项在jclasslib中的位置是General Information中
如果一个class文件被设置成了接口,那么接口和抽象类表示被同时使能,而枚举、super和final标识被关闭;否则,此字节码文件可以使能上表中除了注解外的所有标识。当然,final和抽象两个标识互斥,不能同时使能
synthetic表示此类或接口由编译器生成,没有对应源代码;注解类型必须使能annotation和interface
enum表示此类或父类是枚举类型
类索引、父类索引、接口索引集合
访问标识后,是此类的类别、父类类别和实现的接口,含义如下表所示
这几个两字节的项就确定了类及其继承或实现的信息
在我们的例子中,这几项如下图所示
第一项03,对应常量表中第三项,那就是test/Demo;第二项04,对应常量表中第四项,也就是java/lang/Object。这俩说明了当前类名是test/Demo,父类是Object。第三项为0,表示没有实现接口,这和我们源码中类的定义一致
如果我们的类实现了接口,那么第三项就不为0,且后面紧跟着就是接口索引集合,长度和第三项值一致,而且集合中每一项的顺序和类声明时接口定义的顺序一致。这里我们没有实现接口,00 00后面紧跟着的就不是接口索引集合,而是字段表集合
字段表集合
字段表集合用来描述接口或类中声明的类变量或实例级变量,不包括方法内、代码块内生命的局部变量。字段叫什么名字、是什么类型都是不固定的,只能引用常量池中的常量。
字段表中不会列出从父类或实现的接口中继承过来的字段,只会显示自己本身的字段。因此第一部分的示例三中子类的字节码中,只会列出自己的x,不会列出父类的x。但是内部类会添加外部类的字段,这是为了保证对外部类的访问性
java语言中字段没有重载一说,因此同一个类中两个字段的名字绝不能一样,即便其修饰符不同;但是对于字节码来说,只要描述符不一样,两个字段的可以重名。也就是说字节码中字段可以重名,代码中不行
字段表第一部分还是两字节的计数器,表示字段表长度,也就是当前字节码文件中有几个字段;第二部分就是字段表,每一项的头两个字节是字段的访问标识,访问标识表如下所示,不互斥的标志可以通过|组合,就像访问标识一样
再往后就是当前项对应字段的字段名和类型在常量表中的引用索引,以及属性计数器,这几个分别用俩字节表示。如果属性计数器值不为0,那紧随其后的就是属性表,这一点和方才的类接口信息类似
此例中的字段表集合如下图所示
显然,字段表长度为1,因此字段表就一项。后面的00 02是此字段的访问标识,为私有的;再往后00 05和00 06就是此字节码文件中的字段表。对应常量池中的num和I,也就是int型的num;合起来的private int num,这正是Demo类中唯一的字段。而int又没有别的属性,所以属性计数器为0,后面跟着的就不是属性列表了,而是方法表
字段的属性(以常量字段为例)包括:属性名索引、属性长度和常量值索引,对于常量,属性长度恒为2。属性通用结构如下图所示
方法表集合
方法表描述每个方法的签名,包括修饰符、返回值、参数列表等,只要一个方法不是抽象的或本地的,都会在字节码中体现出来。和字段表中一样,方法表中不会描述从父类或接口中继承或实现的方法,而且会有一些编译器添加的方法,包括类的初始化方法clint和实例的初始化方法init。
java语言中,重载方法只要求方法名和参数列表不一样,因此代码中方法的特征签名包括方法名和参数列表;但字节码中的方法特征签名也包含返回类型,也就是说字节码中,方法名、参数列表和返回类型有一个不一样,多个方法就可以共存。
方法表头俩字节还是方法表长度,意为此字节码文件中有几个方法。然后就是方法表项,每一项的格式如下所示
方法的访问标识符及其含义如下表所示,同样也可以用|组合
此例中的方法表集合如下图所示
头俩字节值为2,说明此此字节码文件的方法表中有俩方法,往后就是那俩方法的描述信息,从00 01 00开始,到0D 00 00 结束。
第一个方法头俩字节表示访问修饰符,这里值为1,也就是public。后面跟着分别俩字节的方法名索引和俩字节的描述符索引,值为7和8,对应常量表中的<init>和()V,也就是void类型的Demo实例构造方法。再往后是两个字节的属性计数器,这里为1,说明就1个属性。
从这儿开始,就要解析属性了,而JVM所有预定义的属性如下表所示
属性 |
位置 |
含义 |
ConstantValue |
字段表 |
编译期定义的常量值 |
Code |
方法表 |
字节码指令 |
StackMapTable |
Code属性 |
|
Exceptions |
方法表 |
异常 |
InnerClasses |
类文件 |
内部类列表 |
EnclosingMethod |
类文件 |
仅当一个类为局部类或匿名类时才拥有这个属性,以标识这个类所在的外围方法 |
Synthetic |
类文件 字段表 方法表 |
编译器自动生成的内容 |
Signature |
类文件 字段表 方法表 |
支持泛型的签名 |
SourceFile |
类文件 |
源文件 |
SourceDebugExtension |
类文件 |
用于存储额外的调试信息 |
LineNumberTable |
Code属性 |
源码行号表 |
LocalVariableTable |
Code属性 |
局部变量表 |
LocalVariableTypeTable |
Code属性 |
局部变量类型表 |
Deprecated |
类文件 字段表 方法表 |
被声明为deprecated的方法或字段 |
RuntimeVisibleAnnotations |
类文件 字段表 方法表 |
运行时可见的注解 |
RuntimeInvisibleAnnotations |
类文件 字段表 方法表 |
运行时不可见的注解 |
RuntimeVisibleParameterAnnotations |
方法表 |
运行时可见的参数注解 |
RuntimeInvisibleParameterAnnotations |
方法表 |
运行时不可见的参数注解 |
RuntimeVisibleTypeAnnotations |
类文件 字段表 方法表 |
运行时可见的类型注解 |
RuntimeInvisibleTypeAnnotations |
类文件 字段表 方法表 |
运行时不可见的类型注解 |
AnnotationDefault |
方法表 |
用于记录注解类元素的默认值 |
BootstrapMethods |
类文件 |
用于保存invokeddynamic指令引用的引导方式限定符 |
MethodParameters |
方法表 |
方法参数 |
而所有属性的通用格式如下图所示
因此,这里属性表中头俩字符表示属性名索引,对应常量表中第9项,也就是Code,说明这一属性是方法的字节指令;Code属性格式如下图所示
所以随后4个字节表示属性长度,此处为2F,也就是47,因此往后47个字节(00 01 ~ 0D 00 00)就都是Code属性了。而后的两个字节为最大操作数栈深度,这里为1。再往后两个字节表示最大局部变量数,这里还是为1,因为这个实例构造方法的局部变量就一个this。接下来四个字节00 00 00 05表示代码长度,这里为5,那么随后五个字节就是真正的字节指令了,即2A B7 00 01 B1。这里,我们看一下jclasslib中此方法的字节码指令
点击第一个aload_0,可以跳到官网看到它的字节码就是2a
然后第二个invokespecial,对应的字节码正是B7
随后俩字节00 01表示它调用的方法常量引用是哪个,这里是第一个常量,也就是Object的实例构造方法。而最后的B1,正对应的是return指令
字节码指令解析完,再往后俩字节表示异常表长度,这里为0,因此不存在异常。再往后俩字节00 02表示自己的属性数量,为2,那么后面就是Code的两个属性。头一个的前俩字节00 0A表示属性名的常量索引,此处为10,也就是LineNumberTable,其格式如下图所示
往后四个字节00 00 00 06为属性长度,这里为6。往后俩字节为行号表长,这里00 01为1,因此后面俩字节00 00为程序计数器起始位置,这里的0表示它是程序的开头;而后俩字节00 03表示开始行号,也就是代码中的第三行(public class Demo {)。至此,Code的第一个属性结束。我们可以看一下jclasslib中对其的解析是否与我们一致,会发现行号表长、程序计数器起始位置、行号、属性长度完全对得上。
第二个属性,一样的道理,头俩字节00 0B表示属性名的常量索引,这里指向第11个常量项Local Variable Table局部变量表,它的属性格式如下图所示
后四个字节00 00 00 0C表示属性长度,因此往后的12个字节都是这个属性的内容。后面俩00 01为局部变量属性数,此处为1,那么后面的内容就是局部变量们的信息。对于我们这唯一的局部变量,它头俩字节00 00为起始PC,此处为0;随后俩字节00 05为局部变量长度,此处为5(一般来说,起始PC + 局部变量表长 = 当前方法Code属性的代码长度,也就是Code属性的code_length值);再往后两个字节00 0C为当前局部变量名字索引,指向第12个常量,也就是this,证实了我在解析行号表时对这个实例构造方法的局部变量就一个this的猜想;再往后两个00 0D为描述符的常量索引,也就是当前局部变量的签名,此处指向第13个常量项Ltest/Demo;,再往后俩为索引,此处为00 00,因为this就是此方法的第一个局部变量。至此,Code的第二个属性也结束了,同时结束的,还有一个方法——实例的构造方法的结束。我们再看看jclasslib中对其第二个属性的解析,会发现局部变量属性数、表长、程序计数器起始位置、描述符名字、属性长度以及索引,都对得上
第二个方法也是一样的道理,头俩字节为访问修饰符,这里的2表示private。再往后跟着的是方法名索引和描述符索引,此处为00 0E和00 0F,对应常量表中第14和第15项,分别是add和()I,也就是整型的方法add。再往后还是俩字节的属性计数器,此处值为1。唯一的属性的属性名还是常量表中的第9项Code,随后四个字节00 00 00 3D表示属性长度,即61,正好范围是00 03 00 01到00 0D 00 00,这部分也是add方法的Code属性表,解析方法和第一个方法一致,此处不再赘言
属性表集合
字节码文件中最后一部分:00 01到最后的00 01就是属性表集合了,负责描述字节码文件的辅助信息,包括class文件的源文件名,以及任何代用RetentionPolicy.CLASS或者Retention.RUNTIME的注解,这些信息通常用于jvm的验证、运行和java程序的调试。当然,前面字段表、方法表也有自己的属性。
我们再看一下最后的属性表描述了哪些内容
再贴一下字节码文件格式
可见,方法信息结束后,头俩字节表示附加属性数,这里00 01表示就一个附加属性,而属性通用格式再看一下
所以头俩字节00 10为属性名常量项的索引,这里是第16个常量SourceFile,那么SourceFile的属性格式如下图所示
属性名索引往后四个字节00 00 00 02表示属性长度,这里为2(其实是个固定值)
最后俩字节00 11就是源文件名的常量索引项17,此处为Demo.java。
至此,Demo.class字节码文件解析完毕,从上面的解析过程可以看到常量池表是多么重要,其他部分都有对它的引用,包括自己也引用自己的东西。
用javap解析class文件
我们可以用javap来解析class文件,可以看到字节码文件路径、修改日期、MD5、源文件名、类信息、常量池、非私有的方法信息和源文件名,但不会显示clinit、父类索引、接口索引集合
如果要看所有的字段和方法签名,可以用-p选项
看一下javap的所有参数
必要的话,可以对不同的参数进行组合使用
-p -v组合可以看到私有方法和属性的相关信息
其实还是推荐jclasslib去看字节码文件,因为显示的内容又全,界面又友好
结语
下一篇文章我们总结一下JVM的字节码指令