JVM之字节码技术

2022/12/9 JVM

# 1 类文件结构

一个简单的 HelloWorld.java 文件

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
1
2
3
4
5

将其执行 javac -parameters -d . HelloWorld.java 编译后的 HelloWorld.class 文件如下(Notepad++使用十六进制进行查看)

1670046032461

根据 JVM 规范,类文件结构如下,这是在 JVM 运行中的所有字节码文件都需要遵循的

class-file {
    u4             magic;	//魔数
    u2             minor_version;	//小版本号
    u2             major_version;	//大版本号
    u2             constant_pool_count;	//常量池中常量个数+1
    cp_info        constant_pool[constant_pool_count-1];	//常量池
    u2             access_flags;	//类的访问控制符标识(public,static,final,abstract等)
    u2             this_class;	//该类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info)
    u2             super_class;	//父类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info)
    u2             interfaces_count;	//接口数量
    u2             interfaces[interfaces_count];	//接口的描述(每个都是对常量池的引用)
    u2             fields_count;	//变量数,包括该类中或接口中类变量和实例变量
    field_info     fields[fields_count];	//变量表集合
    u2             methods_count;	//方法数,包括该类中或接口中定义的所有方法
    method_info    methods[methods_count];	//方法表集合
    u2             attributes_count;	//属性数,包括InnerClasses,EnclosingMethod,SourceFile等
    attribute_info attributes[attributes_count];	//属性表集合
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 1.1 魔数

0-3 字节,表示它是否是【class】类型的文件

1670046677428

# 1.2 版本

4-7 字节,表示类的版本 00 34(52) 表示是 Java 8

1670046787159

# 1.3 常量池

8-9 字节,表示常量池长度,00 1f (31) 表示常量池有 #1- #30 项,注意 #0 项不计入,也没有值,可以发现一个 Class 文件中最多具备 65535(2^16) 个常量,它包括了以下所有类型。

1670047109146

常量池类型映射表,目前为止 JVM 一共定义了 14 种类型的常量。

Constant Type Value Length(Byte) Tip
CONSTANT_Class 7 2 Class信息
CONSTANT_Fieldref 9 4 成员变量信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType
CONSTANT_Methodref 10 4 方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType
CONSTANT_InterfaceMethodref 11 4 接口方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType
CONSTANT_String 8 2 字符串常量名称
CONSTANT_Integer 3 4 int值
CONSTANT_Float 4 4 float值
CONSTANT_Long 5 8 long值
CONSTANT_Double 6 8 double值
CONSTANT_NameAndType 12 4 名称和类型信息,前2为 命名信息,后2为 类型信息
CONSTANT_Utf8 1 2 长度记录了该字符串的长度,通过该记录的长度来读取后续相应字节长度信息
CONSTANT_MethodHandle 15 - 表示方法句柄(后续遇到再补充)
CONSTANT_MethodType 16 - 表示方法类型(后续遇到再补充)
CONSTANT_InvokeDynamic 18 - 表示一个动态方法调用点(后续遇到再补充)

根据以上映射表来对其余字节码的分析

常量池中 #1 项 0a(10),由映射表得知是一个 Method 信息,其通过后续的 00 06(6) 和 00 11(17) 分别表示该方法的【所属类】引用常量池中 #6 项、【方法名】引用常量池中 #17 项。

1670048081295

常量池中 #2 项 09(9),由映射表得知是一个 Field 信息,其通过后续的 00 12(18) 和 00 13(19) 分别表示该成员变量的【所属类】引用常量池中 #18 项、【成员变量名】引用常量池中 #19 项。

1670048493839

常量池中 #3 项 08(8),由映射表得知是一个字符串常量名称,其通过后续的 00 14(20)表示其引用了常量池中 #20 项。

1670048714141

依此类推进行其他常量池的解析,这里就不演示了,了解一个解析形式即可。

以上分析结果的常量池大概就是使用反编译后的常量池结果,而且可以看到所有的字符串常量池中的信息一一对应

#1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
#2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
#3 = String             #20            // Hello World
#4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class              #23            // HelloWorld
#6 = Class              #24            // java/lang/Object
#7 = Utf8               <init>
#8 = Utf8               ()V
#9 = Utf8               Code
#10 = Utf8               LineNumberTable
#11 = Utf8               main
#12 = Utf8               ([Ljava/lang/String;)V
#13 = Utf8               MethodParameters
#14 = Utf8               args
#15 = Utf8               SourceFile
#16 = Utf8               HelloWorld.java
#17 = NameAndType        #7:#8          // "<init>":()V
#18 = Class              #25            // java/lang/System
#19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
#20 = Utf8               Hello World
#21 = Class              #28            // java/io/PrintStream
#22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V
#23 = Utf8               HelloWorld
#24 = Utf8               java/lang/Object
#25 = Utf8               java/lang/System
#26 = Utf8               out
#27 = Utf8               Ljava/io/PrintStream;
#28 = Utf8               java/io/PrintStream
#29 = Utf8               println
#30 = Utf8               (Ljava/lang/String;)V
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

# 1.4 访问标识与继承信息

解析完常量池后的8的字节,就是该部分的信息

  • 2:该class类型和访问修饰符Flag
  • 2:通过常量池找到本类全限定名
  • 2:通过常量池找到父类全限定名
  • 2:表示接口数量
Flag Name Value Interpretation
ACC_PUBLIC 0x0001 public 访问权限
ACC_FINAL 0x0010 final 修饰符
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction. 父类调用方式
ACC_INTERFACE 0x0200 interface 类型
ACC_ABSTRACT 0x0400 abstract 类型
ACC_SYNTHETIC 0x1000 通过合成/生成的代码,没有源代码
ACC_ANNOTATION 0x2000 annotation 类型
ACC_ENUM 0x4000 enum 类型

# 1.5 Field 信息

解析完以上的信息的后续2字节,表示成员变量的数量,此案例文件并没有成员变量

FieldType Type Interpretation
B byte
C char
D double
F float
I int
J long
LClassName; reference 引用类型
S short
Z boolean
[ reference 一维数组

# 1.6 Method 信息

解析完以上的信息的后续2字节,表示方法的数量

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

就是解析为以下反编译文件(使用十六进制分析过程过于庞大)

{
  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

  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
    MethodParameters:
      Name                           Flags
      args
}
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

# 1.7 附加属性

解析为以下反编译文件的内容

SourceFile: "HelloWorld.java"
1

参考文献 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

# 2 字节码指令

# 2.1 入门

分析方法中的字节码指令信息

构造方法对应的字节码信息如下图,对应的反编译信息如下文本块所示

1670054933674

  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
1
2
3
4
5
6
7
8
9
10

有以下对应关系

  • 2a => aload_0:加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  • b7 => invokespecial:预备调用构造方法,哪个方法呢?
  • 00 01:引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】
  • b1:表示返回

main方法对应的字节码信息如下图,对应的反编译信息如下文本块所示

1670055197971

  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
    MethodParameters:
      Name                           Flags
      args
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

有以下对应关系

  • b2 => getstatic:用来加载静态变量,哪个静态变量呢?
  • 00 02:引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  • 12 => ldc:加载参数,哪个参数呢?
  • 03:引用常量池中 #3 项,即 【String Hello World】
  • b6 => invokevirtual:预备调用成员方法,哪个方法呢?
  • 00 04:引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  • b1:表示返回

请参考 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5 (opens new window)

1670055453251

# 2.2 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件

使用命令 javap -v HelloWorld.class,查看完整的反编译信息

Classfile /G:/Idea_workspace/JVM/src/main/java/HelloWorld.class
  Last modified 2022-12-3; size 462 bytes
  MD5 checksum f882f9f216fff39f1b95dd6b16b209a7
  Compiled from "HelloWorld.java"
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            // Hello World
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            // HelloWorld
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               MethodParameters
  #14 = Utf8               args
  #15 = Utf8               SourceFile
  #16 = Utf8               HelloWorld.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Class              #25            // java/lang/System
  #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #20 = Utf8               Hello World
  #21 = Class              #28            // java/io/PrintStream
  #22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V
  #23 = Utf8               HelloWorld
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = 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

  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
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "HelloWorld.java"
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

# 2.3 图解方法执行流程

# 1) 原始 Java 代码

package com.zhuhjay.demo3;
public class Demo3_1 {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}
1
2
3
4
5
6
7
8
9

# 2) 编译后的字节码文件

使用命令 javap -v Demo3_1.class 进行反编译,查看字节码

Classfile /G:/Idea_workspace/JVM/target/classes/com/zhuhjay/demo3/Demo3_1.class
  Last modified 2022-12-3; size 619 bytes
  MD5 checksum 2f036be3ec6ee9204c53f83b17dacc1f
  Compiled from "Demo3_1.java"
public class com.zhuhjay.demo3.Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // com/zhuhjay/demo3/Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/zhuhjay/demo3/Demo3_1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo3_1.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               com/zhuhjay/demo3/Demo3_1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public com.zhuhjay.demo3.Demo3_1();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhuhjay/demo3/Demo3_1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 12: 10
        line 13: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo3_1.java"
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
85
86
87
88
89
90
91
92
93

# 3) 常量池载入运行时常量池

JVM 通过类加载器将以上字节码数据读入,将常量池数据存储到运行时常量池(属于方法区的一部分)中

  • #3 这个数据的来源是 Short.MAX_VALUE + 1,java源码中比较小的数值并不会存储在常量池中,而是与方法的字节码指令存储在一起,一旦超出了 Short.MAX_VALUE(32767) 就会存储到常量池中。
1670221697470

# 4) 方法字节码载入方法区

1670222538332

# 5) main 线程开始运行,分配栈帧内存

会根据以下信息来进行栈帧分配,操作数栈深度为2,局部变量表长度为4

stack=2, locals=4, args_size=1
1
1670222781062

# 6) 执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
    • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    • ldc 将一个 int 压入操作数栈
    • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
1670223128743

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1
1670223335018 1670223376535

以上操作代表源代码的 int a = 10; 这个赋值操作

ldc #3

  • 从常量池加载 #3 数据到操作数栈
  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的(常量折叠优化)
1670223577473

istore_2

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 2
1670223846251

以上操作代表源代码的 int b = Short.MAX_VALUE + 1; 这个赋值操作

iload_1

  • 将局部变量表中的 slot 1 数据读取复制到操作数栈中
1670223953960

iload_2

  • 将局部变量表中的 slot 2 数据读取复制到操作数栈中
1670224060014

iadd

  • 将操作数栈中的数据进行相加的操作
1670224223804 1670224265031

istore_3

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 3
1670224443009

以上操作代表源代码的 int c = a + b; 这个操作

getstatic #4

  • 从常量池中获取成员变量 #4 找到堆中的对象,将该对象的引用放入到操作数栈中
1670224527319

iload_3

  • 将局部变量表中的 slot 3 数据读取复制到操作数栈中
1670224818125

invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等,每执行一个方法都会有新的栈帧)
  • 传递参数,执行新栈帧中的字节码
1670225026514
  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容
1670225112947

return

  • 完成 main 方法调用,弹出 main 栈帧

  • 程序结束

# 2.4 练习 - 分析i++

从字节码角度分析 i++ 相关题目

源码

package com.zhuhjay.demo3;
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}
1
2
3
4
5
6
7
8
9

字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 18
        line 12: 25
        line 13: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
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

分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算

  • a++ 和 ++a 的区别是 先执行 iload 还是 先执行 iinc

  • 将一个 byte 数值类型 10 压入到操作数栈中

1670225735794
  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1,此时完成了代码 int a = 10 的操作
1670225834483
  • 将局部变量表中的 slot 1 的数据读取复制到操作数栈中,a++ 执行顺序是先读取复制,再进行自增的操作
1670225887463
  • 对局部变量表中的 slot 1 进行长度为 1 的自增,是直接在局部变量表中直接进行操作,而不是在操作数栈中进行
  • 此时局部变量表中的 a 和 操作数栈中的数值不是一个相等的值
1670225973371
  • 现将进行 ++a 的运算,会先对局部变量表中的 slot 1 中的数值进行自增,而后才将该结果读取复制到操作数栈中
1670226003812
  • 读取局部变量表中 slot 1 的数值到操作数栈中
1670226078219
  • 计算完 a++++a 过后,操作数栈中已经存在两个数据了,所以会先对操作数栈中的数据进行相加的操作
1670226107200
  • 现将进行 a-- 的运算,先将局部变量表中 slot 1 读取复制到操作数栈中
1670226176345
  • 将局部变量表中的 slot 1 进行自减操作
1670226329153
  • 将操作数栈中的数据进行相加,到此为止 a++ + ++a + a-- 的运算结束
1670226379924
  • 最后将操作数栈中的数据存放到局部变量表中对应的位置上去
1670226433126

# 2.5 条件判断指令

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null
  • 几点说明:

    • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
    • goto 用来进行跳转到指定行号的字节码
  • 源码

    package com.zhuhjay.demo3;
    public class Demo3_3 {
        public static void main(String[] args) {
            int a = 0;
            if (a == 0) {
                a = 10;
            } else {
                a = 20;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 字节码

    • 比较小的数是通过 iconst 指令
    • ifne 指令判断操作数栈中的两个数值是否不相等,如果不相等,则跳转到序号为 12 的指令,否则继续往下执行
    • 该程序 if 判断有两个执行路径
      • 3 -> 6 -> 8 -> 9 -> 15:执行到 goto 指令后会跳转到对应的序号指令继续执行
      • 3 -> 12 -> 14 0> 15:通过了 if 判断跳转到对应的序号指令继续执行
    0: iconst_0
    1: istore_1
    2: iload_1
    3: ifne          12
    6: bipush        10
    8: istore_1
    9: goto          15
    12: bipush        20
    14: istore_1
    15: return
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

Tip:

​ 以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?

参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp (opens new window)

# 2.6 循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

package com.zhuhjay.demo3;
public class Demo3_4 {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }
}
1
2
3
4
5
6
7
8
9

字节码,通过指令 if_icmpge 判断是否大于等于,是则跳出当前 while 循环,否则继续自增

0: iconst_0
1: istore_1
2: iload_1
3: bipush        10
5: if_icmpge     14
8: iinc          1, 1
11: goto          2
14: return
1
2
3
4
5
6
7
8

再比如 do while 循环:

package com.zhuhjay.demo3;
public class Demo3_5 {
    public static void main(String[] args) {
        int a = 0;
        do {
            a++;
        } while (a < 10);
    }
}
1
2
3
4
5
6
7
8
9

字节码,与 while 循环不同的地方就是,do while 会先进行自增操作,之后才进行判断

0: iconst_0
1: istore_1
2: iinc          1, 1
5: iload_1
6: bipush        10
8: if_icmplt     2
11: return
1
2
3
4
5
6
7

最后再看看 for 循环:

package com.zhuhjay.demo3;
public class Demo3_6 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) { }
    }
}
1
2
3
4
5
6

字节码,会发现 for 循环编译后的字节码和 while 循环是一模一样

0: iconst_0
1: istore_1
2: iload_1
3: bipush        10
5: if_icmpge     14
8: iinc          1, 1
11: goto          2
14: return
1
2
3
4
5
6
7
8

# 2.7 练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:

  • 问题出现在 x = x++,这部分代码的字节码运行流程如下
    • 从局部变量表中先获取 x 的数值并复制到操作数栈中,此时:slot x:0; stack x:0
    • 对局部变量表中的 x 所在的槽位数值自增1,此时:slot x:1; stack x:0
    • 将操作数栈中的值赋值给局部变量表中 x ,此时:slot x:0; stack null
    • 一直重复以上操作,x 的结果永远都是 0
  • 若将以上问题代码改为 x = ++x,将不会出现以上的问题
package com.zhuhjay.demo3;
public class Demo3_7 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x); // 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.8 构造方法

# 1) <clinit>()V

public class Demo3_8 {
    static int i = 10;
    static {
        i = 20;
    }
    static {
        i = 30;
    }
}
1
2
3
4
5
6
7
8
9

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法<clinit>()V ,该方法会在类加载的初始化阶段被调用

static {};
   descriptor: ()V
   flags: ACC_STATIC
   Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return
1
2
3
4
5
6
7
8
9
10
11
12

# 2) <init>()V

public class Demo3_9 {
    private String a = "s1";
    {
        b = 20;
    }
    private int b = 10;
    {
        a = "s2";
    }
    public Demo3_9(String a, int b) {
        this.a = a;
        this.b = b;
    }
    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9("s3", 30);
        System.out.println(d.a);	// s3
        System.out.println(d.b);	// 30
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

public com.zhuhjay.demo3.Demo3_9(java.lang.String, int);
   descriptor: (Ljava/lang/String;I)V
   flags: ACC_PUBLIC
   Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // 调用父类的构造方法(Object)
         4: aload_0
         5: ldc           #2                  // <- "s1"
         7: putfield      #3                  // -> this.a
        10: aload_0
        11: bipush        20				  // <- 20
        13: putfield      #4                  // -> this.b
        16: aload_0
        17: bipush        10				  // <- 10
        19: putfield      #4                  // -> this.b
        22: aload_0
        23: ldc           #5                  // <- "s2"
        25: putfield      #3                  // -> this.a
        28: aload_0
        29: aload_1							  // <- "s3"
        30: putfield      #3                  // -> this.a
        33: aload_0
        34: iload_2							  // <- 30
        35: putfield      #4                  // -> this.b
        38: return
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

# 2.9 方法调用

看一下几种不同的方法调用对应的字节码指令

public class Demo3_10 {
    public Demo3_10() { }
    private void test1() { }
    private final void test2() { }
    public void test3() { }
    public static void test4() { }
    public static void main(String[] args) {
        Demo3_10 d = new Demo3_10();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        Demo3_10.test4();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

字节码

0: new           #2                  // class com/zhuhjay/demo3/Demo3_10
3: dup
4: invokespecial #3                  // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4                  // Method test1:()V
12: aload_1
13: invokespecial #5                  // Method test2:()V
16: aload_1
17: invokevirtual #6                  // Method test3:()V
20: aload_1
21: pop
22: invokestatic  #7                  // Method test4:()V
25: invokestatic  #7                  // Method test4:()V
28: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈

  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量

  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于 静态绑定

  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】

  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了

  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

# 2.10 多态的原理

package com.zhuhjay.demo3;
public class Demo3_11 {
    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }
    public static void main(String[] args) throws IOException {
        test(new Dog());
        test(new Cat());
        System.in.read();
    }
}
abstract class Animal {
    public abstract void eat();
    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}
class Dog extends Animal {
    @Override
    public void eat() { System.out.println("啃骨头"); }
}
class Cat extends Animal {
    @Override
    public void eat() { System.out.println("吃鱼"); }
}
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

# 1) 运行代码

注意:运行代码前需要添加虚拟机参数 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers,禁止指针压缩,省去了地址换算

停在 System.in.read() 方法上,这时运行 jps 获取进程 id

# 2) 运行 HSDB 工具

进入 JDK 目录,执行命令 java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面后 Attach 到对应的 进程id,效果如下

1670338229940

# 3) 查找内存中的对象

打开 Tools -> Find Object By Query 工具

输入 select d from com.zhuhjay.demo3.Dog d 并执行,语法同 SQL 一般,查询结果就是在内存中存在的 Dog 对象内存地址。

1670338644448

# 4) 查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针

但目前看不到它的实际地址,在底层是 C++ 的数据结构,可以看到类中的一些数据,子类父类等等,虚引用表长度 _vtable_len

1670338857348

# 5) 查看对象的内存地址

通过 Windows -> Console 进入命令行模式,执行 mem 0x0000023338896cd0 2

  • mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

在结果中的第二行即为 Class 的内存地址

1670339471963

# 6) 查看类的 vtable

  • 方法一:Alt+R 进入 Inspector 工具,输入刚刚查询到的 Class 内存地址

    1670339740477
  • 方法二:打开 Tools -> Class Browser 输入 Dog 查找,也一样可以找到该 Class 对应的内存地址,然后使用方法一进行查询

    1670339913417

无论通过哪种方法,都可以找到 Dog Classvtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

0x00000233675d3c90
			 0x1b8 +
---------------------
0x00000233675d3e48
1
2
3
4

然后使用 Windows -> Console 工具执行命令,就能够得到这 6 个虚方法的入口地址了

mem 0x00000233675d3e48 6
0x00000233675d3e48: 0x00000233671d1b10 
0x00000233675d3e50: 0x00000233671d15e8 
0x00000233675d3e58: 0x00000233675d3758 
0x00000233675d3e60: 0x00000233671d1540 
0x00000233675d3e68: 0x00000233671d1678 
0x00000233675d3e70: 0x00000233675d3c38 
1
2
3
4
5
6
7

# 7) 验证方法地址

通过 Tools -> Class Browser 查看每个类的方法定义,比较可知每一个地址都对上了查询到的所有虚方法地址

Dog - public void eat() @0x00000233675d3c38;
Animal - public java.lang.String toString() @0x00000233675d3758;
Object - protected void finalize() @0x00000233671d1b10;
Object - public boolean equals(java.lang.Object) @0x00000233671d15e8;
Object - public native int hashCode() @0x00000233671d1540;
Object - protected native java.lang.Object clone() @0x00000233671d1678;
1
2
3
4
5
6

# 8) 小结

当执行 invokevirtual 指令时

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

# 2.11 异常处理

# 1) try-catch

public class Demo3_12_1 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}
1
2
3
4
5
6
7
8
9
10

部分字节码如下

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,记录异常对象信息
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2) 多个 single-catch 块的情况

public class Demo3_12_2 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 20;
        } catch (NullPointerException e) {
            i = 30;
        } catch (Exception e) {
            i = 40;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

部分字节码信息如下

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 Slot 2 位置被共用
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        20
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        30
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        40
        25: istore_1
        26: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
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

# 3) multi-catch 的情况

public class Demo3_12_3 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException | NullPointerException | NumberFormatException e) {
            i = 20;
        }
    }
}
1
2
3
4
5
6
7
8
9
10

部分字节码如下

  • 将异常入口进行一次收集,局部变量表 Slot 2 只收集一个,并且是这些异常的父类型
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5     8   Class java/lang/NullPointerException
             2     5     8   Class java/lang/NumberFormatException
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/RuntimeException;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
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

# 4) finally

public class Demo3_12_4 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

部分字节码如下

  • 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
  • JVM 会自动捕捉其没有被 catch 块捕获的异常,且如果真的捕获到了这部分异常,同样也会执行 finally 中的代码,并且使用 athrow 指令将异常向外抛出
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1			// 0 -> i
         2: bipush        10	// try ----------------------
         4: istore_1			// 10 -> i		    		|
         5: bipush        30	// finally 		    		|
         7: istore_1			// 30 -> i		    		|
         8: goto          27	// return -------------------	
        11: astore_2			// catch Exception -> e	-----
        12: bipush        20	// 							|
        14: istore_1			// 20 -> i 					|
        15: bipush        30	// finally					|
        17: istore_1			// 30 -> i					|
        18: goto          27	// return -------------------
        21: astore_3			// catch any -> slot 3 ------
        22: bipush        30	// finally 					|
        24: istore_1			// 30 -> i					|
        25: aload_3				// <- slot 3				|
        26: athrow				// throw --------------------
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I
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

# 2.12 练习 - finally 面试题

# 1) finally 出现了 return

public class Demo3_13_1 {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

部分字节码如下

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
public static int test();
   descriptor: ()I
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=2, args_size=0
         0: bipush        10	// <- 10 放入栈顶
         2: istore_0			// 10 -> slot 0 (从栈顶移除)
         3: bipush        20	// <- 20 放入栈顶 (finally代码)
         5: ireturn				// 返回栈顶 int(20)
         6: astore_1			// catch any -> slot 1
         7: bipush        20	// <- 20 放入栈顶 (finally代码)
         9: ireturn				// 返回栈顶 int(20)
      Exception table:
         from    to  target type
             0     3     6   any
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

若将案例代码改为如下

  • 制造一个异常 int i = 1/0,运行后发现不会有异常出现,且结果与上个例子相同
public class Demo3_13_1 {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            int i = 1/0;
            return 10;
        } finally {
            return 20;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2) finally 对返回值影响

public class Demo3_13_2 {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        int i = 0;
        try {
            return i;
        } finally {
            i = 100;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

部分字节码如下

  • finally 中不带 return,就可以正常的捕获异常了(从字节码中 athrow 看出)
public static int test();
   descriptor: ()I
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=1, locals=3, args_size=0
         0: iconst_0			// <- 0 放入栈顶
         1: istore_0			// 0 -> i
         2: iload_0				// <- i(0)
         3: istore_1			// 0 -> slot 1,暂存至 slot 1,目的是为了固定返回值
         4: bipush        100	// <- 100 放入栈顶
         6: istore_0			// 100 -> i
         7: iload_1				// <- i(0),读取返回值放置栈顶
         8: ireturn				// 返回栈顶 i(0)
         9: astore_2			// catch 部分
        10: bipush        100
        12: istore_0
        13: aload_2
        14: athrow
      Exception table:
         from    to  target type
             2     4     9   any
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      13     0     i   I
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

# 2.13 synchronized

public class Demo3_14 {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
1
2
3
4
5
6
7
8

部分字节码如下

  • JVM 会自动给 synchronized 代码块中的代码进行异常捕获,当捕获到异常或者 synchronized 代码块运行结束时,都会进行锁的释放,并且锁的释放过程也会进行异常的捕获,确保锁的释放
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // new Object
         3: dup								  // 复制一份,用来初始化
         4: invokespecial #1                  // invokespecial -> <init>:()V
         7: astore_1						  // 将 Object 引用 -> lock
         8: aload_1							  // <- lock (synchronized开始)
         9: dup								  // 复制一份,一个用于加锁指令一个用于解锁指令
        10: astore_2						  // 将 Object 引用 -> slot 2
        11: monitorenter					  // 加锁指令
        12: getstatic     #3                  // <- System.out
        15: ldc           #4                  // <- "ok"
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2							  // <- slot 2(解锁指令使用的 lock引用)
        21: monitorexit						  // 解锁指令
        22: goto          30
        25: astore_3						  // catch any -> slot 3
        26: aload_2							  // <- slot 2(解锁指令使用的 lock引用)
        27: monitorexit						  // 解锁指令
        28: aload_3
        29: athrow							  // 异常抛出
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
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

注意:方法级别的 synchronized 不会在字节码指令中有所体现

# 3 编译器处理

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

# 3.1 默认构造器

public class Candy1 { }
1

编译成class后的代码:

public class Candy1 {
    // 这个无参构造是编译器帮助我们加上的
    public Candy1() {
        super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
    }
}
1
2
3
4
5
6

# 3.2 自动拆装箱

这个特性是 JDK 5 开始加入的:代码一

public class Candy2 {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}
1
2
3
4
5
6

这段代码在 JDK 5 之前是无法编译通过的,必须改写为:代码二

public class Candy2 {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}
1
2
3
4
5
6

显然之前版本的代码太麻烦了,需要在基本类型和包装类型直接来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成,即 代码一 都会在编译阶段被转为 代码二

# 3.3 泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 实际调用的是 List.add(Object e)
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    }
}
1
2
3
4
5
6
7

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
1
2

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
1
2

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

  • JVM 将代码 Integer x = list.get(0); 通过 Object obj = List.get(int index); 方式获取出来后,会使用 checkcast 指令对照泛型信息表来进行类型的转换
public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;
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

使用反射,仍然能够获得这些信息(只能获取到方法参数上和返回值的泛型信息):

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {return null}
1
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始类型 - " + parameterizedType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
        	System.out.printf("\t泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

输出

原始类型 - interface java.util.List
	泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
	泛型参数[0] - class java.lang.Integer
	泛型参数[1] - class java.lang.Object
1
2
3
4
5

# 3.4 可变参数

可变参数也是 JDK 5 开始加入的新特性: 例如:

public class Candy4 {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
    	foo("hello", "world");
    }
}
1
2
3
4
5
6
7
8
9

可变参数 String... args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同样 java 编译器会在编译期间将上述代码变换为:

public class Candy4 {
    public static void foo(String[] args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
    	foo(new String[]{"hello", "world"});
    }
}
1
2
3
4
5
6
7
8
9

注意 如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

# 3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        for (int e : array) {
        	System.out.println(e);
        }
    }
}
1
2
3
4
5
6
7
8

字节码信息如下

  • 数组赋初值的简化写法也是语法糖
  • 数组的 foreach 会被编译成 fori
public class Candy5_1 {
    public Candy5_1() { }
    public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10

而集合的循环:

public class Candy5_2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer i : list) {
        	System.out.println(i);
        }
    }
}
1
2
3
4
5
6
7
8

实际被编译器转换为对迭代器的调用:

  • 使用迭代器进行遍历(只有实现了 Iterable 接口的集合才能使用 foreach 语法糖 )
public class Candy5_2 {
    public Candy5_2() { }
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            Integer e = (Integer)iter.next();
            System.out.println(e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

注意 foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其 中 Iterable 用来获取集合的迭代器( Iterator

# 3.6 switch 字符串

JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello":
                System.out.println("h");
                break;
            case "world":
                System.out.println("w");
                break;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

注意 switch 配合 String 和枚举使用时,变量不能为 null,原因分析完语法糖转换后的代码应当自然清楚

会被编译器转换为:

public class Candy6_1 {
    public Candy6_1() { }
    public static void choose(String str) {
        byte x = -1;
        switch(str.hashCode()) {
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                	x = 0;
                }
                break;
            case 113318802: // world 的 hashCode
                if (str.equals("world")) {
                	x = 1;
                }
        }
        switch(x) {
            case 0:
            	System.out.println("h");
            	break;
            case 1:
            	System.out.println("w");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCodeequals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BMC. 这两个字符串的 hashCode 值都是 2123 ,如果有如下代码:

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "BM":
                System.out.println("h");
                break;
            case "C.":
                System.out.println("w");
                break;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

编译结果为

public class Candy6_1 {
    public Candy6_1() { }
    public static void choose(String str) {
        byte var2 = -1;
        switch(str.hashCode()) {
        case 2123:
            if (str.equals("C.")) {
                var2 = 1;
            } else if (str.equals("BM")) {
                var2 = 0;
            }
        default:
            switch(var2) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
            }

        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.7 switch 枚举

switch 枚举的例子,原始代码

public enum Sex {
    MALE, FEMALE;
}
1
2
3
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男");break;
            case FEMALE:
                System.out.println("女");break;
        }
    }
}
1
2
3
4
5
6
7
8
9
10

编译后会生成以下文件

  • 其中 Candy7$1.class 文件是用来映射枚举类 Sex 的一个静态内部类

1670415162524

字节码如下

static final int[] $SwitchMap$com$zhuhjay$jit$Sex;
   descriptor: [I
   flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

static {};
   descriptor: ()V
   flags: ACC_STATIC
   Code:
      stack=3, locals=1, args_size=0
         0: invokestatic  #1                  // Method com/zhuhjay/jit/Sex.values:()[Lcom/zhuhjay/jit/Sex;
         3: arraylength
         4: newarray       int
         6: putstatic     #2                  // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
         9: getstatic     #2                  // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
        12: getstatic     #3                  // Field com/zhuhjay/jit/Sex.MALE:Lcom/zhuhjay/jit/Sex;
        15: invokevirtual #4                  // Method com/zhuhjay/jit/Sex.ordinal:()I
        18: iconst_1
        19: iastore
        20: goto          24
        23: astore_0
        24: getstatic     #2                  // Field $SwitchMap$com$zhuhjay$jit$Sex:[I
        27: getstatic     #6                  // Field com/zhuhjay/jit/Sex.FEMALE:Lcom/zhuhjay/jit/Sex;
        30: invokevirtual #4                  // Method com/zhuhjay/jit/Sex.ordinal:()I
        33: iconst_2
        34: iastore
        35: goto          39
        38: astore_0
        39: return
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

也就是会生成以下代码

public class Candy7 {
    /**
     * 定义一个合成类(仅 jvm 使用,对我们不可见)
     * 用来映射枚举的 ordinal 与数组元素的关系
     * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
     */
    static class Candy7$1 {
        // 数组大小即为枚举元素个数,里面存储case用来对比的数字
        static final int[] $SwitchMap$com$zhuhjay$jit$Sex = new int[2];
        static {
            $SwitchMap$com$zhuhjay$jit$Sex[Sex.MALE.ordinal()] = 1;
            $SwitchMap$com$zhuhjay$jit$Sex[Sex.FEMALE.ordinal()] = 2;
    	}
    }
    public static void foo(Sex sex) {
        int x = Candy7$1.$SwitchMap$com$zhuhjay$jit$Sex[sex.ordinal()];
        switch (x) {
            case 1:
                System.out.println("男");
            	break;
            case 2:
                System.out.println("女");
                break;
        }
    }
}
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

# 3.8 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例

public enum Sex {
    MALE, FEMALE;
}
1
2
3

转换后的代码如下

  • 继承 Enum 枚举类
  • final 修饰类
  • 实例个数都在本类中创建,并且不可修改,属于常量
  • 私有构造方法,保护枚举类
  • Enum.valueOf(Sex.class, name) 底层原理就是 Map,通过键查找对应的枚举
public final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    
    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }
    
    private Sex(String name, int ordinal) {
    	super(name, ordinal);
    }
    
    public static Sex[] values() {
    	return $VALUES.clone();
    }
    
    public static Sex valueOf(String name) {
    	return Enum.valueOf(Sex.class, name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.9 try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamOutputStreamConnectionStatementResultSet 等接口都实现了 AutoCloseable ,使用 try-with-resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9 {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html")) {
            System.out.println(is);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9

编译后代码如下

  • 更完整的关流操作,甚至可以保留压制异常 var2.addSuppressed(var11);
public class Candy9 {
    public Candy9() { }
    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html");
            Throwable var2 = null;

            try {
                System.out.println(is);
            } catch (Throwable var12) {
                // var2 是我们代码出现的异常
                var2 = var12;
                throw var12;
            } finally {
                if (is != null) {
                    // 如果我们代码有异常
                    if (var2 != null) {
                        try {
                            is.close();
                        } catch (Throwable var11) {
                            // 当关流的时候也出现了异常,那么作为压制异常添加
                            var2.addSuppressed(var11);
                        }
                    } else {
                        // 我们代码没有异常,关流出现了异常,那么 var14 关流的异常
                        is.close();
                    }
                }
            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }
    }
}
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

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

public class Test1 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) {
            int i = 1/0;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        throw new Exception("关流异常");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

执行以上代码有以下结果,可以发现异常信息被完整的保留了下来

java.lang.ArithmeticException: / by zero
	at com.zhuhjay.jit.Test1.main(Test1.java:10)
	Suppressed: java.lang.Exception: 关流异常
		at com.zhuhjay.jit.MyResource.close(Test1.java:20)
		at com.zhuhjay.jit.Test1.main(Test1.java:11)
1
2
3
4
5

# 3.10 方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {
    public Number m() {
        return 1;
    }
}
class B extends A {
    @Override
    public Integer m() {
        return 2;
    }
}
1
2
3
4
5
6
7
8
9
10
11

对于子类,java 编译器会做如下处理:

class B extends A {
    public Integer m() {
    	return 2;
    }
    // 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        // 调用 public Integer m()
        return m();
    }
}
1
2
3
4
5
6
7
8
9
10

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:

for (Method declaredMethod : B.class.getDeclaredMethods()) {
    System.out.println(declaredMethod);
}
1
2
3

发现有两个 m() 方法,证明了字节码中桥接方法的存在

public java.lang.Integer com.zhuhjay.jit.B.m()
public java.lang.Number com.zhuhjay.jit.B.m()
1
2

# 3.11 匿名内部类

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10

源代码编译后会生成一个额外的类

// 额外生成的类
final class Candy11$1 implements Runnable {
    Candy11$1() { }
    public void run() {
    	System.out.println("ok");
    }
}
1
2
3
4
5
6
7
public class Candy11 {
    public static void main(String[] args) {
    	Runnable runnable = new Candy11$1();
    }
}
1
2
3
4
5

引用局部变量的匿名内部类,源代码:

public class Candy11 {
    public static void main(final String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok" + args.length);
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10

会在额外生成的类中添加一个有参构造方法,通过参数的方式传递给该类的成员变量的存储,后续让方法获取成员变量的信息来达到目的

// 额外生成的类
final class Candy11$1 implements Runnable {
    final String[] val$args;
    Candy11$1(String[] args) {
        this.val$args = args;
    }
    public void run() {
    	System.out.println("ok" + val$args.length);
    }
}
1
2
3
4
5
6
7
8
9
10
public class Candy11 {
    public static void main(final String[] args) {
    	Runnable runnable = new Candy11$1(args);
    }
}
1
2
3
4
5

注意 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 args 的值赋值给了 Candy11$1 对象的 val$args 属性,所以 args 不应该再发生变化,如果变化了,那么 val$args 属性没有机会跟着一起变化。(当不对变量进行修改时,编译器会将局部变量用 final 修饰)