深入理解Java虚拟机(三)

Java 字节码

代码编译结果从本地机器码转变为字节码,是存储格式发展的一小步,确是编程语言发展的一大步。

字节码文件剖析

我们从一段简单的代码来入手

1
2
3
4
5
6
7
8
9
10
11
12
public class MyTest01 {

private int a = 0;

public int getA() {
return a;
}

public void setA(int a) {
this.a = a;
}
}

我要要看一下 java 文件对应的 class 文件的结构,定位到工程的 out\production\classes 下边执行:

javap -c com.cuzz.jvm.bytecode.Mytest01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
警告: 二进制文件com.cuzz.jvm.bytecode.Mytest01包含com.cuzz.jvm.bytecode.MyTest01
Compiled from "MyTest01.java"
public class com.cuzz.jvm.bytecode.MyTest01 {
public com.cuzz.jvm.bytecode.MyTest01();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field a:I
9: return

public int getA();
Code:
0: aload_0
1: getfield #2 // Field a:I
4: ireturn

public void setA(int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
}

我们如果需要获得更多信息可以使用如下命令:

javap -verbose com.cuzz.jvm.bytecode.Mytest01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
警告: 二进制文件com.cuzz.jvm.bytecode.Mytest01包含com.cuzz.jvm.bytecode.MyTest01
Classfile /E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/com/cuzz/jvm/bytecode/Mytest01.class
Last modified 2019-2-3; size 492 bytes
MD5 checksum cceeac51ae7b6fc46c60faf834de5932
Compiled from "MyTest01.java"
public class com.cuzz.jvm.bytecode.MyTest01
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 // com/cuzz/jvm/bytecode/MyTest01.a:I
#3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/cuzz/jvm/bytecode/MyTest01
#23 = Utf8 java/lang/Object
{
public com.cuzz.jvm.bytecode.MyTest01();
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 a:I
9: return
LineNumberTable:
line 8: 0
line 10: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cuzz/jvm/bytecode/MyTest01;

public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/cuzz/jvm/bytecode/MyTest01;

public void setA(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 a:I
5: return
LineNumberTable:
line 17: 0
line 18: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/cuzz/jvm/bytecode/MyTest01;
0 6 1 a I
}
SourceFile: "MyTest01.java"

我们也可以使用二进制文件查看器查看class文件的16进制信息(winhex下载):

1549161558276

16文件查看器里边第一行的CA 就是一个字节的容量(8位bit):

使用 javap -verbos 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法信息、类变量与成员变量等信息。

魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE (詹姆斯.高斯林设计的,蕴意:咖啡宝贝,java 的图标是咖啡。

魔数之后的4个字节为版本信息,前2个字节表示 minor versio(次版本号),后两个字节表示 major version(主版本号)。 这里的版本号为 00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。

字节常量池剖析

常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个 Java 类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是 Class 文件的资源仓库,比如说 Java 类中定义的方法与变量信息,都是存储在常量池中。常量池中的主要储存两类常量:字面量与符号引用。字面量如文本字符串,Java 中声明为 final 的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。

常量池的总体结构:Java 类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成。常量池数量紧跟在主版本号后面,占据 2 个字节;常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素类型、结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是一个 u1 类型,该字节是一个标志位,占据 1 个字节。JVM 在解析常量池时,会根据这个 u1 类型来获取元素的具体类型。

值得注意的是,常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不使用)。对应的是 00 18 转化为十进制为24个常量,而我们看到只有23个。目的是满足某些常量池索引值的数据在特定情况下需要表达“不引用任何一个常量”的含义;根本原因在于,索引 0 也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应 null 值,所以,常量池的索引从 1 开始而不是 0 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/cuzz/jvm/bytecode/MyTest01.a:I
#3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/cuzz/jvm/bytecode/MyTest01
#23 = Utf8 java/lang/Object

Class 文件结构中常量池数据类型的结构表

c717816f04e3fee14c9745e06356247a

在 JVM 规范中,每一个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回的 void 类型都是用一个大写字符来表示,对象类型则使用字符 L 加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM 都只使用一个大写字母来表示,如下所示:B - byte,C - char,D - double,F - float,I - int,J - long,S - short,Z - boolean,V - void,L - 对象类型,如 Ljava/lang/String;

对于数组类型来说,没一个维度使用前置 [ 来表示,如 int [] 被记录为 [I ,String[][] 被记录为 [[Ljava/lang/String;

用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组括号内,如方法:String getRealNameByIdAndNickName(int id, String name) 的描述符为:(I, Ljava/lang/String;) Ljava/lang/String;

我们来分析前面几个常量,如图:

1549183205091

我反编译出来的文件对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/cuzz/jvm/bytecode/MyTest01.a:I
#3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/cuzz/jvm/bytecode/MyTest01
#23 = Utf8 java/lang/Object
  1. 0A 00 04 00 14,如图中的标注出来,0A 对应值为10,在上表的常量中 CONSTANT_Methodref_info 中,那么后边的2个字节 00 04 (十进制4)就是 U2(第一个index),即指向声明方法的类描述符 CONSTANT_Class_info 的索引项,而第二个索引(第二个index)00 14(十进制20) 指向名称及类型描述符 CONSTANT_NameAndType_info 的索引项。类描述指向 #4 ,#4 又指向 #23,所以描述为 java/lang/Object,而名称以及类型描述符指向 #20,#20 有指向 #7 和 #8,"<init>":()V 表示为构造方法。
  2. 09 00 03 00 15 ,09 是标志位对用的是 CONSTANT_Fieldref_info,第一个索引指向的是声明字段的类或接口描述符,CONSTANT_Class_info 的索引项,根上面一样分析。
  3. 07 00 16 , 00 16 十进制是22 ,07是常量 CONSTANT_CLass_info,只有一个index,指向的是指定权限定名常量项的索引, 00 16 是十进制22。
  4. 07 00 17 ,07是常量 CONSTANT_CLass_info,只有一个index,指向的是指定权限定名常量项的索引,00 17 十进制是23。
  5. 01 00 01 61,01 是 CONSTANT_Utf8_info,后面 00 01 这两个字节表示长度,最后 61 (十进制为97)的表示 ASCII 中带索引,在 ASCII 中为字母 a。
  6. 01 00 01 为 I。
  7. 等等

Java 字节码结构

class-structure

Class 字节码中有两种数据类型

  • 字节数据直接量:这是基本的数据类型,共细分为 u1、u2、u4、u8 这四种,分别代表连续的 1 个字节、2 个字节、4 个字节和8 个字节。
  • 表(数组):表示有多个基本数据或其他表,按照既定顺序组成的大的数据集合。表示有结构的,它的结构体现在,组成表的成分所在的位置和顺序都已经严格定义好的。

访问标志

访问标志(Access_Flag)信息包括该 Class 文件是类还是接口,是否被定义成 public,是否是 abstract,如果是类,是否被声明成 final。通过上面的源代码,我们可以知道该文件是类并且是 public。

access-flag

常量池之后两个字节就是访问标志,我们这个类中是 0x 00 21 ,从上面来看并没有,原来它是 0x 00 200x 00 01 的并集,表示 ACC_PUBLIC 与 ACC_SUPER。

类索引、父类索引与接口索引

1549250272126

  • 00 03 是类索引,指向 #3 表示是一个类,其名字为 com/cuzz/jvm/bytecode/MyTest01
  • 00 04 是父亲索引,指向 #4 表示是一个类,其名字是 java/lang/Object
  • 00 00 是接口,表示没有接口

字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但不包括方法内部声明的局部变量。

1549339719719

如下图

1549340431752

00 01 是成员变量的数量,后面接着就是 field_info 成员变量信息

1
2
3
4
5
6
7
field_info {
u2 access_flags; // 0002 表示私有 private
u2 name_index; // 0005 表示 a
u2 descriptor_index; // 0006 表示 I
u2 attributes_count; // 0000 没有
attribute_info attributes[attributes_count];
}

方法表

刚开始的 00 03 表示有三个方法,除了getter/setter 还有默认构造方法

1
2
3
4
5
6
7
methods_count {
u2 access_flags; // 0001 表示 public
u2 name_index; // 0007 指向常量池中 #7 的常量为 <init>
u2 descriptor_index; // 0008 指向常量池中 #8 的常量为 ()V
u2 attributes_count; // 0001 表示一个属性
attribute_info attributes[attributes_count];
}

方法中的属性结构

1
2
3
4
5
attribute_info {
u2 attribute_name_index; // 0009 指向常量池中 #9 为 Code
u4 attribute_length; // 0000 0038 表示长度为 0x38 为 56 长度的字节
u1 info[attribute_length];
}

Code 结构

Code attribute 的作用是保存该方法的结构,如所对应的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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_length 表示 attribute 所包含的字节数,不包含 attribute_name_index 和 attribute_length 字段
  • max_stack 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
  • max_locals 表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
  • 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 时,表示处理所有的异常

字节码查看工具

https://github.com/ingokegel/jclasslib

-------------本文结束感谢您的阅读-------------

本文标题:深入理解Java虚拟机(三)

文章作者:cuzz

发布时间:2019年02月06日 - 23:02

最后更新:2019年11月01日 - 15:11

原始链接:http://blog.cuzz.site/2019/02/06/深入理解Java虚拟机(三)/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

请博主吃包辣条
0%