我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结构。
准备工作
1.编写一个简单的源码
public class helloworld {
public static void main(String args[]){
System.out.println("Hello World!");
}
}
2.将java源码转化为.class文件
javac helloworld.java
3.以16进制的形式查看class文件
在Windows系统下需要一个可查看16进制的编辑器,因此我用linux系统生成了以下内容
方法:
①vim helloworld.class ,然后输入:%!xxd 就是以16进制显示class文件
②xxd helloworld.class helloworld.txt,然后cat helloworld.txt就可以显示16进制的class文件
0000000: cafe babe 0000 0034 0022 0a00 0600 1409 .......4."......
0000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................
0000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
0000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
0000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc
0000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
0000060: 0004 7468 6973 0100 164c 6578 616d 706c ..this...Lexampl
0000070: 655f 312f 6865 6c6c 6f77 6f72 6c64 3b01 e_1/helloworld;.
0000080: 0004 6d61 696e 0100 1628 5b4c 6a61 7661 ..main...([Ljava
0000090: 2f6c 616e 672f 5374 7269 6e67 3b29 5601 /lang/String;)V.
00000a0: 0004 6172 6773 0100 135b 4c6a 6176 612f ..args...[Ljava/
00000b0: 6c61 6e67 2f53 7472 696e 673b 0100 0a53 lang/String;...S
00000c0: 6f75 7263 6546 696c 6501 000f 6865 6c6c ourceFile...hell
00000d0: 6f77 6f72 6c64 2e6a 6176 610c 0007 0008 oworld.java.....
00000e0: 0700 1c0c 001d 001e 0100 0c48 656c 6c6f ...........Hello
00000f0: 2057 6f72 6c64 2107 001f 0c00 2000 2101 World!..... .!.
0000100: 0014 6578 616d 706c 655f 312f 6865 6c6c ..example_1/hell
0000110: 6f77 6f72 6c64 0100 106a 6176 612f 6c61 oworld...java/la
0000120: 6e67 2f4f 626a 6563 7401 0010 6a61 7661 ng/Object...java
0000130: 2f6c 616e 672f 5379 7374 656d 0100 036f /lang/System...o
0000140: 7574 0100 154c 6a61 7661 2f69 6f2f 5072 ut...Ljava/io/Pr
0000150: 696e 7453 7472 6561 6d3b 0100 136a 6176 intStream;...jav
0000160: 612f 696f 2f50 7269 6e74 5374 7265 616d a/io/PrintStream
0000170: 0100 0770 7269 6e74 6c6e 0100 1528 4c6a ...println...(Lj
0000180: 6176 612f 6c61 6e67 2f53 7472 696e 673b ava/lang/String;
0000190: 2956 0021 0005 0006 0000 0000 0002 0001 )V.!............
00001a0: 0007 0008 0001 0009 0000 002f 0001 0001 .........../....
00001b0: 0000 0005 2ab7 0001 b100 0000 0200 0a00 ....*...........
00001c0: 0000 0600 0100 0000 0700 0b00 0000 0c00 ................
00001d0: 0100 0000 0500 0c00 0d00 0000 0900 0e00 ................
00001e0: 0f00 0100 0900 0000 3700 0200 0100 0000 ........7.......
00001f0: 09b2 0002 1203 b600 04b1 0000 0002 000a ................
0000200: 0000 000a 0002 0000 0009 0008 0016 000b ................
0000210: 0000 000c 0001 0000 0009 0010 0011 0000 ................
0000220: 0001 0012 0000 0002 0013 ..........
附上对照表:
类型 | 名称 | 说明 | 长度 |
---|---|---|---|
u4固定值 | magic | 魔数,识别Class文件格式 | 4个字节 |
u2 | minor_version | 副版本号 | 2个字节 |
u2 | major_version | 主版本号 | 2个字节 |
u2 | constant_pool_count | 常量池计算器 | 2个字节 |
cp_info常量表 | constant_pool | 常量池 | n个字节 |
u2 | access_flags | 访问标志 | 2个字节 |
u2 | this_class | 类索引 | 2个字节 |
u2 | super_class | 父类索引 | 2个字节 |
u2 | interfaces_count | 接口计数器 | 2个字节 |
u2 | interfaces | 接口索引集合 | 2个字节 |
u2 | fields_count | 字段个数 | 2个字节 |
field_info字段表 | fields | 字段集合 | n个字节 |
u2 | methods_count | 方法计数器 | 2个字节 |
method_info方法表 | methods | 方法集合 | n个字节 |
u2 | attributes_count | 附加属性计数器 | 2个字节 |
attribute_info属性表 | attributes | 附加属性集合 | n个字节 |
注:一个字节=8位2进制数=2位16进制数
说明一下:class文件只有两种数据类型:无符号数和表。如下表所示:
数据类型 | 定义 | 说明 |
---|---|---|
无符号数 | 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。 | 其中无符号数属于基本的数据类型。以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节 |
表 | 表是由多个无符号数或其他表构成的复合数据结构。 | 所有的表都以“_info”结尾。由于表没有固定长度,所以通常会在其前面加上个数说明。 |
实际上整个class文件就是一张表,其结构就是上面的表一了。
开始解读
1、魔数
咖啡宝贝是jvm识别文件是否为.class的关键
类型 | 名称 | 说明 | 长度 | 16进制数 |
---|---|---|---|---|
u4固定值 | magic | 魔数,识别Class文件格式 | 4个字节 | cafe babe |
2、版本号
类型 | 名称 | 说明 | 长度 | 16进制数 | 10进制数 |
---|---|---|---|---|---|
u2 | minor_version | 副版本号 | 2个字节 | 0000 | 0 |
u2 | major_version | 主版本号 | 2个字节 | 0034 | 52 |
主版本号52代表1.8,所以jdk的版本为1.8.0
3、常量池
常量池主要存放两大类常量:字面量和符号引用。如下表:
常量 | 具体的常量 |
---|---|
字面量 | 文本字符串 |
声明为final的常量值 | |
符号引用 | 类和接口的全限定名 |
字段的名称和描述符 | |
方法的名称和描述符 |
类型 | 名称 | 说明 | 长度 | 16进制数 | 10进制数 |
---|---|---|---|---|---|
u2 | constant_pool_count | 常量池计算器 | 2个字节 | 0022 | 34 |
常量池是从1开始,因为第0号常量被jvm占用,表示什么都不引用,所以应该有33个常量
类型 | 名称 | 说明 | 长度 | 备注 |
---|---|---|---|---|
cp_info常量表 | constant_pool | 常量池 | n个字节 | 第一个字节是tag位,后面的内容要查手册 |
附上手册:
开始解读常量池
第一个常量:
tag-->0a(10)--> CONSTANT_Methodref_info
class_info2个字节--->00 06(#6)--->java/lang/Object
nameAndType2个字节---00 14(#20)---> "<init>":()V-->表示构造方法,没有入参,返回值为null
第二个常量:
tag-->09(9)--->Fieldref
class_info2个字节-->0015(#21)-->java/lang/System
nameAndType2个字节---00 16(#22)--->out:Ljava/io/PrintStream;
第三个常量:
tag-->08(8)--->String
index2个字节-->00 17(#23)-->Hello World!
.......
第七个常量:
tag-->01(1)->utf8
length-->00 06(6)
byte-->3c 696e 6974 3e-->转换成ASCLL码--><init>
第八个常量:
tag-->01(1)->utf8
length-->00 03(3)
byte-->2829 56-->转换成ASCLL码-->()V
接着也是依次按照手册解读
实际上我们在class文件的目录下敲一行简单的命令:javap -verbose helloworld,就可以得到以下内容
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // example_1/helloworld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lexample_1/helloworld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 helloworld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 example_1/helloworld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
这里说几个字符串
#9 Code:存放JVM指令
#10 LineNumberTable:JVM指令与源码的映射关系(也是我们IDE可以找到我们错误源码行数的原因)
#11 LocalVariableTable:栈帧中局部变量表
{
public example_1.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 7: 0 //表示code第0条指令对应第7行
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lexample_1/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 Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 22: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "helloworld.java"
4、访问标志
类型 | 名称 | 说明 | 长度 | 16进制 |
---|---|---|---|---|
u2 | access_flags | 访问标志 | 2个字节 | 0021 |
0x0021是通过位运算得出的:
5、类索引、父类索引、接口索引
1、访问标志后的两个字节就是类索引(This class name)
0005-->#5-->example_1/helloworld
2、类索引后的两个字节就是父类索引(super class name)
0006 -->#6-->java/lang/Object
3、父类索引后的两个字节则是接口索引计数器
0000-->即没有任何接口索引
通过这三项,就可以确定了这个类的继承关系了。
。。。。。
后面还有字段、方法、附加属性,我累了,不想解读了
附上两个资料:
博客:https://blog.csdn.net/u011810352/article/details/80316870
视频:https://www.bilibili.com/video/av79500296