Java字节码深度剖析
Java字节码文件查看
我们有一个类Test01,具体内容如下:
package bytecode;
public class Test01 {
private int i = 0;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
编译这个类,得到Test01.class文件
IDE查看
用IDEA编译器查看
我们发现查看到的class文件与类文件基本相同,这是因为IDE自带的Fernflower decompiler将字节码文件反编译的结果。我们可以在插件市场查找安装jclasslib插件,来在IDEA中查看class文件。
hexedit查看
通过hexedit直接查看该字节码文件
扩展:hexedit安装
输入:sudo apt install hexedit
当你启动它时,你必须指定要打开的文件的位置,然后它会为你打开它。
javap -verbose查看
通过javap指令查看字节码文件:javap -verbose ****
执行javap -verbose
指令,得到的结果如下:
Classfile /home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/bytecode/Test01.class
Last modified 2019-12-3; size 460 bytes
MD5 checksum 7913e827b66fbb2c05907b76dafa32ec
Compiled from "Test01.java"
public class bytecode.Test01
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // bytecode/Test01.i:I
#3 = Class #22 // bytecode/Test01
#4 = Class #23 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lbytecode/Test01;
#14 = Utf8 getI
#15 = Utf8 ()I
#16 = Utf8 setI
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 Test01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // i:I
#22 = Utf8 bytecode/Test01
#23 = Utf8 java/lang/Object
{
public bytecode.Test01();
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_0
6: putfield #2 // Field i:I
9: return
LineNumberTable:
line 7: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lbytecode/Test01;
public int getI();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field i:I
4: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lbytecode/Test01;
public void setI(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field i:I
5: return
LineNumberTable:
line 15: 0
line 16: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lbytecode/Test01;
0 6 1 i I
}
SourceFile: "Test01.java"
使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
Java字节码文件结构剖析
Class字节码中只有两种数据类型:
- 字节数据直接量:属于基本的数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节组成的整体数据。可以用来描述数字、索引引用、数量值、或者按照UTF-8编码的字符串值。
- 表(数组):由多个基本元素或者其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在组成表的成分所在的位置和顺序都是已经严格定义好的。
魔数(magic)
所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE
版本号(version)
魔数之后的4个字节是版本信息,前2个字节表示次版本号(minor version),后两个字节为主版本号(major version)。这里的版本号为0x 00 00 00 34,表示次版本号为0,主版本号为52(这里的52值的其实是jdk8,如果是jdk7的话就是51)。与指令返回的版本号一致。
minor version: 0
major version: 52
常量池(Constant pool)
紧接着主版本号之后的,就是常量池入口。一个Java类定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。
常量池中主要两类常量:
- 字面量:如文本字符串,Java中声明为final的常量值等
- 符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等
常量池的总体结构:
Java类所对应的常量池主要由常量池数量与常量池表这两部分组成:
- 常量池数量紧跟在主版本号之后,占据两个字节
- 常量池表则紧跟在常量池数量之后
常量池表与一般的数组不同的是,常量池表中不同的元素的类型、结构和长度都是不同的;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标识位,占据一个字节。JVM在解析产量池时,会根据这个u1类型来获取元素的具体类型。
值得注意的是:常量池表中元素个数 = 常量池数量 – 1。其中0暂不使用,目的是满足某些常量池索引值的数据在特定情况下表达【不引用任何常量池】的含义;根本原因在于索引0也是一个常量(保留常量),只不过它不位于常量表,这个常量就对应null值,所以常量池的索引是从1开始的。在本例中,0x 00 18代表常量池数量,常量池数量为24 – 1 = 23个,与javap的结果相同。
目前,常量池中出现的常量类型有14种,如下表:
后面三种都是Java7之后出现的,关于动态引用的。我们主要看前面11种,下表给出了前11个常量类型的详细结构说明:
有了这两张表就可以继续剖析常量池的内容了。每一个常量的第一个字节都是标志位,第一个常量的标志为0x 0A,转换为十进制为10,表示常量类型为:CONSTANT_Methodref_info,按照上表,第一个index为2个字节,第二index也为2个字节,0x 0A 00 04 00 14这五个字节表示常量池中第一个常量,第一个index值为4,第二个index值为20,与javap反编译的结果一致:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
。
指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项为4,4对应的是:#4 = Class #23 // java/lang/Object
指向字段描述符的CONSTANT_NameAndType_info的索引项为20,20对应的是:#20 = NameAndType #7:#8 // "<init>":()V
,这个又指向7和8:
#7 = Utf8 <init>
#8 = Utf8 ()V
PS:
- 在JVM规范中,每个变量、字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和的代表无返回值的void类用都用一个大写字符表示,对象类型则使用大写字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,JVM都只使用一个大写字母表示,如下所示:
- B – byte
- C – char
- D – double
- F – float
- I – int
- J – Long
- Z – boolean
- V – void
- L – 对象类型,如Ljava/lang/String;
- 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为:[I,String[][]被记录为:[[Ljava/lang/String;
- 用描述符描述方法时,按照先参数列表、后返回值的顺序来描叙。参数列表按照参数的严格顺序放在一组()之内,如方法:String test( int id, String name)的描述符为:(I, Ljava/lang/String;)Ljava/lang/String;
字节码访问标识(access_flags)
紧跟着常量池的是字节码访问标识,占据两个字节。访问标志信息包括该Class文件是类还是接口,是否被定义为public,是否是abstract,如果是类,是否被声明成final。
本文例中0x 00 21是0x 0020与0x0001的并集,表示ACC_PUBLIC与ACC_SUPER。
类索引与父类索引
紧跟着字节码访问标识的是类索引和父类索引,分别占据两个字节。
本例中,0x 00 03表示类索引,在常量池中对应:#3 = Class #22 // bytecode/Test01
0x 00 04表示父类索引,在常量池中对应:#4 = Class #23 // java/lang/Object
接口(interfaces)
紧跟着父类索引的是接口,接口由两部分组成:
- 接口个数:接口个数占据两个字节,本例中,接口个数为0x 00 00
- 接口名:接口个数为0时,接口名不会出现,如果接口个数大于等于一,接口名才会出现,占据两个字节
字段(fields)
紧跟着接口的是字段,字段用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
字段由两部分组成:
-
字段个数:字段个数占据两个字节,本文例中为0x 00 01,表示只有一个字段
-
字段表:字段表由4部分组成:
- access_flags:访问标识,占据两个字节,本文例中为0x 00 02,表示为ACC_PRIVATE
- name_index:名字,占据两个字节,本文例中为0x 00 05,对应值为:** #5 = Utf8 i**
- descriptor_index:描述符,占据两个字节,本文例中为0x 00 06,对应值为:** #6 = Utf8 I**
- attributes_count:属性个数,占据两个字节,本文例中为0x 00 00
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法(method)
紧跟着字段的是方法,方法由两部分组成:
- 方法个数:方法个数占据2个字节,本例中为0x 00 03,表示方法表中将由三个方法
- 方法表:方法表的结构如下:
方法表
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法中的每个属性都是一个attribute_info结构,方法的属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info[attribute_length] | 1 |
JVM预定义了部分的attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用。不同的attribute通过attribute_name_index来区分。
Code结构
Code attribute的作用是保存该方法的结构,如所对应的字节码:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
-
attribute_name_index:属性名索引
-
attribute_length:表示attribute属性所包含的字节数,不包含attribute_name_index和attribute_length字段
-
max_stack:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
-
max_locals:表示方法执行期间创建的局部变量的数目,包含用来表达传入的参数的局部变量
-
code_length:表示该方法所包含的字节码的字节数以及具体的指令码
-
code[code_length]:具体的指令码,是该方法被调用时,虚拟机所执行的字节码
-
exception_table:存放的是处理异常的信息,每个exception_table表项由start_pc、end_pc、handler_pc、catch_type组成
- start_pc和end_pc表示在code数组中的从start_pc和end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
- handler_pc表示处理异常的代码的开始处
- catch_type表示会被处理的异常类型,它指常量池中的一个异常类。当catch_type为0时,表示处理所有的异常
-
attributes_count:属性值
- 行号表(LineNumberTable):这个属性用来表示code数组的字节码和Java代码行数之间的关系,可以用来在调试的时候定位代码执行的行数
LineNumberTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; }line_number_table[line_number_table_length]; }
- 局部变量表(LocalVariableTable):
LocalVariableTable_attribute { u2 attribute_name_index; u4 attribute_length; // 不包括起始6个字节的属性长度。 u2 local_variable_table_length; // local_variable_table表中的项数。 { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; }
本文例字节码方法解读
如图所示,方法从0x 00 03,表示本字节码文件由三个方法,这里将会仔细解读第一个方法:
- 0x 00 01:access_flags,对应值为ACC_PUBLIC
- 0x 00 07:name_index,对应常量池中的值为:
- 0x 00 08:descriptor_index,对应常量池中的值为:()V
-
0x 00 01:attributes_count,表示这个方法有一个属性值
- 0x 00 09:attribute_name_index,对应常量池中的值为:Code,表示这是一个Code attribute
- 0x 00 00 00 38:attribute_length,属性表长度,表示这个属性的长度为56个字节
- 0x 00 02:max_stack
- 0x 00 01:max_locals
- 0x 00 00 00 0A:code_length,表示code的长度为10个字节
-
0x 2A B7 00 01 2A 03 B5 00 02 B1:code[code_length]
- 0x 2A:aload_0
- 0x B7 00 01:invokespecial #1
- 0x 2A:aload_0
- 0x 03:iconst_0
- 0x B5 00 02:putfield #2
- 0x B1:return
- 0x 00 00:exception_table_length
-
0x 00 02:attributes_count,表示该Code attribute有两个属性
-
0x 00 0A:attribute_name_index,对应常量池中的值为:LineNumberTable
-
0x 00 00 00 0A:attribute_length,表示这个行号表的长度为10
- 0x 00 02:line_number_table_length,表示两对映射
- 0x 00 00 00 07:字节码偏移量为0,映射到源代码的行号为7
- 0x 00 04 00 08:字节码偏移量为4,映射到源代码的行号为8
-
0x 00 00 00 0A:attribute_length,表示这个行号表的长度为10
-
0x 00 0B:attribute_name_index,对应常量池中的值为:LocalVariableTable
- 0x 00 00 00 0C:attribute_length,表示这个行号表的长度为12
- 0x 00 01:局部变量的个数为1
- 0x 00 00 00 0A:开始位置是0,结束位置是10
- 0x 00 0C:局部变量的值,对应常量池的值为:this
- 0x 00 0D:局部变量的描述,对应常量池的值为: Lbytecode/Test01;
- 0x 00 00:索引值
-
0x 00 0A:attribute_name_index,对应常量池中的值为:LineNumberTable
至此,第一个方法解读完毕,可以通过javap的结果对照查看:
public bytecode.Test01();
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_0
6: putfield #2 // Field i:I
9: return
LineNumberTable:
line 7: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lbytecode/Test01;
属性(Attributes)
0x 00 01 00 12 00 00 00 02 00 13
- 0x 00 01:表示包含一个属性
-
0x 00 12:attribute_name_index,对应常量池中的值为:SourceFile
- 0x 00 00 00 02:attribute_length,表示占据两个字节
- 0x 00 13:对应常量池中的值为:Test01.java