目录
前言
对于Java来说,“字节码(Byte Code)”的存储格式是构成“一次编写,到处运行”的平台无关性的基石。Java虚拟机不与包括Java语言在内的任何程序语言绑定,而只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
本文通过彻底解构一段大家耳熟能详的代码(HelloWorld)来一窥JVM虚拟机核心——字节码的“秘密”。
需要使用的工具有:UltraEdit,以及Java bin目录下的javap。
预警:前方多图,继续阅读请谨慎
一、Class文件格式概要
Class文件格式采用的是一种类似C语言结构体的伪结构来存储数据,其只有两种数据类型:“无符号数”和“表”。所谓无符号数是指基本的数据类型,以u1、u2、u4…等分别代表1个字节、2个字节、4个字节的无符号数。它可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
整个Class文件本质上可以看作是一张表,其数据项按严格顺序排列构成,如图一:
下面我们将跟着代码来解读class文件中各类型对应的字节表示。
二、HelloWorld代码
以我们最熟悉的代码行为例:
public class HelloWorld {
public static void main(String[] args){
System.out.println("Hello World!");
}
}
其对应class文件内容(IDEA中翻译显示):
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
通过javap 工具(java自带的解析class文件工具)先从整体上看这个class文件的结构是怎样的,后面也将结合这个结构进行解释,代码一:
$ javap -verbose HelloWorld.class
Classfile HelloWorld.class
Last modified 2020-11-30; size 534 bytes
MD5 checksum f085642c9a175e363b8cde7b90538c83
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
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 // 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 LHelloWorld;
#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 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
{
public 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 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHelloWorld;
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 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
三、字节码解析
下面我们通过UE十六进制方式查看Class文件,并对照“图一”进行分析。
1.Magic Number
魔数,对应类型:u4,头四个字节为:0xCAFEBABE,它唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件,如图所示:
2.Minor Version && Major Version
Class文件的版本号,其中第5、第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version),对应类型都为u2,如图,黄色圈的是次版本号,红色圈的是主版本号。其中版本号0x0034转10进制为52,对应JDK8。
3.常量池
3.1 constant_pool_count
常量池数量,u2类型,对应字节如下图,0x0022 => 34,JVM规定实际数量为该值减一,为33,对照前文中使用javap打印的“代码一”中“Constant pool”也可知。
3.2 constant_pool
常量池中的每一项常量都是一个表,共有17种不同类型的常量,且各类型有大致相同的解读方式。
表一:
表二:
3.1.1 常量例子
接下来我们开始解读第一个常量例子
如图红框所示,0x0A => 10,对应表一为CONSTANT_Methodref_info,对照“代码一”,也可得到印证
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
结合表二对CONSTANT_Methodref_info的结构描述如下:
tag后跟随的2个index为0x0006与0x0014,都是索引项,指向常量池元素6,20:
#6 = Class #27 // java/lang/Object
#20 = NameAndType #7:#8 // "<init>":()V
3.1.2 其他常量
按照例子中的方式,我们可以找出剩下的32个常量结构,并在下图用彩圈框出。其中蓝色圈框的0x01类型(CONSTANT_Utf8_info),如0x01 00 06,为了查看方便,仅框起tag和长度length部分,随后跟着的是对应长度的字符串。
4.访问标志、索引等
4.1 access_flags 访问标志
常量池结束跟着的是2个字节的访问标志(access_flags),其代表的是class整体的访问类型,值由下表对应值通过位运算’|'或计算得到。
如下图,0x0021 = 0x0001 | 0x0020,即 ACC_PUBLIC、 ACC_SUPER:
对应代码一中:
flags: ACC_PUBLIC, ACC_SUPER
4.2 this_class 类索引、super_class 父类索引、interfaces 接口索引集合
继续参照图一,接下来的是this_class(类引用)、super_class(父类索引)、interfaces_count(接口索引集合数量)、interfaces(接口索引集合具体内容),它们的类型都是u2。
this_class : 0x0005 对应常量池
#5 = Class #26 // HelloWorld
super_class:0x0006
#6 = Class #27 // java/lang/Object
interfaces_count:0x0000 => 代表0个接口,于是interfaces也无需表示了。
5. 字段表、方法表、属性表集合
5.1 fields 字段表
其表示顺序为:
- fields_count,u2类型,字段数量
- access_flags,u2类型,访问标志
- name_index,u2类型,名称引用
- descriptor_index,u2类型,一般为字段对应的数据类型
因本文例子中无字段变量,所以fields_count对应 0x0000,且没有其它的额外信息,如图:
5.2 methods 方法、attributes 属性相关解析
接下来是对方法的描述,首先是 methods_count,u2类型,代表的是方法数量,0x0002 => 2,即有两个方法,其中一个是构造方法,另一个是main方法。需要注意的是,方法表和属性表可以看作是一体的,它们共同描述了一个方法的内容,即每一个方法后面都会跟随着相关的属性表。随后我们将逐一对这两个方法进行解析。
5.2.1 方法1中的方法表集合
先从代码一中看整个方法1的内容,后续内容可以此作对照:
public 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 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHelloWorld;
方法表属性顺序为:
- access_flags,u2类型,访问标志
- name_index,u2类型,方法名称
- descriptor_index,u2类型,方法返回类型
- attributes_count,u2类型,属性数量
- attributes_name_index,u2类型,属性名
如图逐项解析
access_flags:0x0001 对应 ACC_PUBLIC
name_index:0x0007 对应常量池中
#7 = Utf8 <init>
descriptor_index:0x0008 对应常量池中
#8 = Utf8 ()V
attributes_count:0x0001 => 1 个属性,
attributes_name_index:0x0009 对应常量池中的’Code’,具体内容在下一节属性表中分析。
#9 = Utf8 Code
5.2.2 方法1中的属性表
虚拟机规范中定义了29项属性,具体见附录1,这些属性会在不同位置出现,对于本文例子,仅包含其中三项: Code属性、LineNumberTable属性、LocalVariableTable属性。另从附录1可知,LineNumberTable属性、LocalVariableTable属性为Code属性的子属性。
5.2.2.1 Code属性:
其中 attribute_name_index已由前述方法中的0x0009表示了,attribute_lenght(属性总长度): 0x0000002F = 47(字节),包含了后续,Code、LineNumberTable、LocalVariableTable属性内容的总和。
max_stack(操作数栈深度最大值):0x0001 = 1
max_locals(局部变量表所需存储空间):0x0001 = 1
code_length(Java源程序表以后生成字节码指令长度):0x00000005 = 5
code(字节码指令具体内容,见附录2): 0x2AB70001B1
0x2A: aload_0 读入指令,将第0个变量槽中为reference类型的本地变量压入到栈顶
0xB7: invokespecial指令,以栈顶reference类型数据所指向对象作为方法接收者,调用此对象的实例构造器方法
0x0001: invokespecial指令的参数,代表一个符号引用(对应常量池#1元素,实例构造器“()”方法的符号引用)
0xB1: return指令
exception_table_length:0x0000 => 0 ,无异常相关信息,所以exception_table也无存在必要
attributes_count:0x0002 => 2,后续包含2个attribute属性(从属于Code的LineNumberTable,LocalVariableTable),这2个attribute的具体内容将在下一节展开
5.2.2.2 LineNumberTable属性
attribute_name_index(属性名称):0x000A => 对应常量池#10,即“LineNumberTable”
attribute_length(属性长度):0x00000006 => 6 (字节)
line_number_table_length:0x0001 = 1
line_number_table包含start_pc(字节码行号),line_number(Java源码行号)两个u2类型数据项
start_pc:0x0000 = 0
line_number:0x0001 = 1
5.2.2.3 LocalVariableTable属性
attribute_name_index(属性名):0x000B => 对应常量池#11,即“LocalVariableTable”
attribute_length(属性总长度):0x00000C => 12字节
local_variable_table_length(栈帧与源码对应表长度):0x0001 = 1
start_pc(局部变量生命周期开始的字节码偏移量):0x0000 = 0
length(局部变量作用范围覆盖长度):0x0005 = 5
name_index(局部变量名称):0x000C => 对应常量池#12,即“this”
descriptor_index(局部变量描述符):0x000D => 对应常量池#13,即“LHelloWorld;”
index(局部变量在栈帧的局部变量表中变量槽的位置,即“Slot”栏位):0x0000 = 0
5.2.3 方法2中的方法表集合
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 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
依照5.2.1中方式得知,
access_flag: 0x0009 => ACC_PUBLIC + ACC_STATIC
name_index: 0x000E => main
descriptor_index: 0x000F => ([Ljava/lang/String;)V
attributes_count: 0x0001 = 1
attribute_name_index: 0x0009 = Code
5.2.4 方法2中的属性表
5.2.4.1 Code属性:
attribute_length(Code总长度,包含子属性):0x00000037 = 55(字节)
max_stack:0x0002 => 2
max_locals:0x0001 => 1
code_length:0x00000005 => 9字节
code:0xB2 00 02 12 03 B6 00 04 B1
0xB2:getstatic 指令,获取静态域 0x0002(对应常量池#2元素),压入到栈顶
0x12:ldc指令,操作常量池#3元素,压入到栈顶
0xB6:invokevirtual指令,调用实例方法 0x0004(对应常量池#4元素,即println方法)
0xB1:return指令
5.2.4.2 LineNumberTable属性
attribute_name_index:0x000A => LineNumberTable
attribute_length:0x0000000A => 10 (字节)
line_number_table_length:0x0002 => 2
- start_pc:0x0000 => 0
line_number:0x0003 => 3 - start_pc:0x0008 => 8
line_number:0x0004 => 4
5.2.4.3 LocalVariableTable属性
attribute_name_index: 0x000B = LocalVariableTable
attribute_length: 0x0000000C = 12
local_variable_table_length: 0x0001 = 1
start_pc: 0x0000 = 0
length: 0x0005 = 9
name_index: 0x0010 = args
descriptor_index: 0x0011 = [Ljava/lang/String;
index: 0x0000 = 0
5.3 SourceFile属性
SourceFile属性是隶属于整个Class文件的,用于记录生成这个Class文件的源代码文件名称。
attributes_count: 0x0001 => 只有1个
attribute_name_index: 0x0012 => 对应常量池#18,即“SourceFile”
attribute_length: 0x00000002 =>该属性内容长度为2
sourcefile_index: 0x0013 => 对应常量池#19,即“HelloWorld.java”
四、结语
至此,我们已将整个Class文件按字节逐一进行了解析,由此可以帮助我们对于Class文件结构有了更形象的认识,也对JVM字节码有了更深入的理解。Java技术能够一直保持着非常良好的向后兼容性,以及卓越的跨平台能力,Class文件结构的稳定的确功不可没。
五、附录
1. 虚拟机规范预定义的属性表
2. 虚拟机字节码指令表
作者:侯嘉逊