封面来源:由博主个人绘制,如需使用请联系博主。

参考链接:黑马程序员JVM快速入门

本文涉及的代码:mofan212/jvm-demo

除特别注明外,本文内容都基于 JDK 1.8

类加载与字节码技术涉及的组件

1. 类文件结构

Oracle Java 8 JVM 规范 class 文件格式:The class File Format

存在 HelloWorld.java

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

使用 javac -parameters -d . HelloWorld.java 命令编译并保留方法中的参数信息。

再使用 od -t xC HelloWorld.class (这是 Linux 环境下的命令,在 Windows 环境下可以用 Git 里执行)查看字节码文件内容:

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
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
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;
attribute_info attributes[attributes_count];
}

1.1 魔数

0~3 字节,表示是否是 class 类型的文件。

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

1.2 版本

4~7 字节,表示类的版本。

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

34 是十六进制,对应十进制 52,表示类的版本是 Java 8。

1.3 常量池

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

8~9 字节,表示常量池长度:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

23 十进制对应 35,表示常量池有 #1 ~ #34 项,#0 项不计入,也没有值。

#1

#10a 对应十进制 10,根据上表查询得知,表示 CONSTANT_Methodref,即方法信息。00 0600 15(21) 表示它引用了常量池中 #6#21 项来获取这个方法的所属类和方法名:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

00 06 对应第 #6 项,表示 Class 信息,然后它又引用了第 #28 项,指明具体的 Class 是 java/lang/Object

00 15 对应第 #21 项,表示方法名、参数类型和返回值类型。它分别引用 #7 项表名方法名是 <init>,即构造方法;然后又引用 #8 项,指定方法描述符为 ()V,即无参无返回值。

#6

#607 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项:

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

#2801 表示一个 utf8 串,00 10(16) 表示长度,指 java/lang/Object

0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 

#21

#210c 表示名和类型,00 07 00 08 又引用了常量池的 #7#8 项:

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

#701 表示一个 utf8 串,00 06 是长度,3c 69 6e 69 74 3e 表示 <init>(构造方法):

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

#801 表示一个 utf8 串,00 03 是长度,28 29 56 表示 ()V,即无参、无返回值:

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

后续常量池字节码内容继续按照此方式进行分析即可,建议:

  • 参考中文 JVM 圣经,周志明老师的《深入理解 Java 虚拟机》
  • 查看字节码文件使用 IDEA 插件 jClassLib

1.4 访问标识与继承信息

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

00 21 (由上表中的 0x00010x0020 相加获得)表示该 class 是一个公共的类:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 

00 05 表示根据常量池中的 #5 项找到 本类 的全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 

00 06 表示根据常量池中的 #6 找到 父类 的全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 

00 00 表示该类实现的接口数量,此处为 0:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 

1.5 Field 信息

00 00 表示成员变量数量,此处为 0:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
FieldType term Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

1.6 Method 信息

00 02 表示方法数量,本类为 2(默认无参构造方法与 main 方法):

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 

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

构造方法

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
  • 红色代表访问修饰符 public
  • 蓝色代表引用了常量池 #07 项作为方法名称,对应 <init>
  • 绿色代表引用了常量池 #08 项作为方法参数描述,对应 ()V,即无参无返回
  • 黄色代表方法属性数量,本方法是 1
  • 红色代表方法属性,其中:
    • 00 09 表示引用了常量池 #09 项,是 Code 属性
    • 00 00 00 2f 表示此属性的长度是 47
    • 00 01 表示 操作数栈 的最大深度
    • 00 01 表示 局部变量表 最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 5
    • 2a b7 00 01 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,对应 LineNumberTable 属性(字节码行号与 Java 代码行号对应,方便调试)
      • 00 00 00 06 表示此属性的总长度,本例是 6
      • 00 01 表示 LineNumberTable 长度
      • 00 00 表示字节码行号,对应 Java 源码行号 00 04
    • 00 0b 表示引用了常量池 #11 项,对应 LocalVariableTable 属性(局部变量表)
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示 LocalVariableTable 长度
      • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 05 表示局部变量覆盖的长度范围
      • 00 0c 表示局部变量名称,本例引用了常量池 #12 项,对应 this
      • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,即 this 类型,当前类的类型
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

main 方法

main 方法分析方式与构造方法一致,不再赘述。

1.7 附加属性

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14
  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即 SourceFile,表示字节码文件对应的 Java 源文件名称
  • 00 00 00 02 表示此属性长度
  • 00 14 表示引用了常量池 #20 项,即 HelloWorld.java

2. 字节码指令

Oracle Java 8 JVM 规范指令集:The Java Virtual Machine Instruction Set

2.1 入门

调用父类 Object 的构造方法

对应字节码指令:

2a b7 00 01 b1
  • 查询 JVM 规范得知,2a 表示 aload_0,即加载 slot 0 的局部变量(加载到操作数栈),对应 this,作为后续执行 invokespecial 构造方法调用的参数
  • b7 表示 invokespecial,预备调用构造方法
  • 00 01 表示需要调用的构造方法,查询常量池 #1 项,得到调用的构造方法信息
  • b1 表示返回

调用 main 方法

对应字节码指令:

b2 00 02 12 03 b6 00 04 b1
  • 查询 JVM 规范得知,b2 对应 getstatic,用于加载静态变量
  • 00 02 引用常量池中 #2 项,表示 getstatic 需要加载的静态变量信息,简单来说是 System.out
  • 12 对应 ldc(load constant),用于加载参数
  • 03 引用常量池中 #3 项,即字符串常量 Hello World
  • b6 对应 invokevirtual,预备调用成员方法
  • 00 04 引用常量池中 #4 项,即 println 方法
  • b1 表示返回

2.2 javap 工具

Oracle 提供了 javap 工具来反编译 class 文件:

1
javap -v HelloWorld.class

-v 参数表示输出 class 文件的详细信息。

2.3 图解方法执行流程

有如下 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package indi.mofan;


/**
* 演示字节码指令和操作数栈、常量池的关系
*
* @author mofan
* @date 2025/10/4 15:39
*/
public class Demo1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

编译 Demo1 后,使用 javap -v class文件 查看字节码文件。

常量池载入运行时常量池

「常量池」指的是 class 文件中的 Constant pool,「运行时常量池」指的是 JVM 内存结构中「方法区」的一部分。

常量池载入运行时常量池

对于 short 范围内的整数不会存入运行时常量池中,而是与字节码指令一起存放。short 的最大值是 32767,加一后大于最大值,因此 32768 会被存放到运行时常量池中。

方法字节码载入方法区

方法字节码载入方法区

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

main线程开始运行分配栈帧内存

通过 javap 命令查看的字节码文件中存在:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4
  • stack=2 表示操作数栈的深度是 2
  • locals=4 表示局部变量表有 4 个槽位

执行引擎开始执行字节码

bipush 10 表示将一个 byte 压入操作数栈(由于操作数栈的宽度占 4 个字节,压入内容不足 4 个字节时,会补齐 4 个字节),类似的指令还有:

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(因为 long 是 8 个字节,需要分两次压入)
  • 小数字和字节码指令存放在一起,超过 short 范围的数字会存入常量池

bipush-10

istore_1 表示弹出操作数栈栈顶数据,存入局部变量表的 slot 1

istore_1-1

istore_1-2

ldc #3 表示从运行时常量池中加载 #3 数据到操作数栈。

注意:Short.MAX_VALUE 对应 32767,加一操作能够在编译器确定,因此运行时常量池中直接存放了 32768

ldc #10

根据前面对 istore_1 的讲解,istore_2 表示弹出操作数栈栈顶数据,存入局部变量表的 slot 2

istore_2-1

istore_2-2

接着从局部变量表里读取参与加法运算的两个数:

  • iload_1 表示从局部变量表 slot 1 里读取数据
  • iload_2 表示从局部变量表 slot 2 里读取数据

iload_1

iload_2

然后执行 iadd 弹出堆操作数栈里的两个整型数据进行相加:

iadd-1

再把运算结果压入操作数栈顶:

iadd-2

istore_3 再弹出操作数栈栈顶数据,存入局部变量表的 slot 3

istore_3-1

istore_3-2

到此,局部变量 abc 均执行完毕。

getstatic #4 从运行时常量池中获取成员变量 System.out 的引用,然后把该对象的 引用 添加进操作数栈中:

getstatic#4-1

getstatic#4-2

在调用 println 方法前,需要先加载所需的参数,使用 iload_3 从局部变量表 slot 3 里读取数据:

iload_3-1

iload_3-2

使用 invokevirtual #5 调用方法打印数据:

  • 找到运行时常量池 #5
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack 等)
  • 传递参数,执行新栈帧中的字节码

invokevirtual #5

目标方法执行完毕后,弹出栈帧。

清除 main 操作数栈的内容:

invokevirtual#5执行完毕

完成 main 方法调用后,弹出 main 栈帧,程序结束。

2.4 练习-分析 a++

目的:从字节码角度分析 a++ 相关题目。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 从字节码角度分析 a++ 相关题目。
*
* @author mofan
* @date 2025/10/4 20:22
*/
public class Demo1_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
// 11
System.out.println(a);
// 34
System.out.println(b);
}
}

得到如下部分字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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

分析:

  • iinc 指令是直接在局部变量 slot 上进行运算的
  • a++++a 的区别是先执行 iload 还是先执行 iinc

a++ 会被分解为两条字节码指令:

  • iload_1
  • iinc 1,1

从字节码指令理解a++

++a 也会被分解为两条字节码指令,内容也与 a++ 一样,但顺序不同:

  • iinc 1,1

  • iload_1

从字节码指令理解a--

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 两个引用是否 !=
  • byteshortchar 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码
1
2
3
4
5
6
7
8
9
10
public class Demo1_3 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}

对应的部分字节码:

 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

以上比较指令中没有涉及 longfloatdouble 的比较,那应该怎么比较它们呢?

可以参考:lcmp

2.6 循环控制指令

循环控制指令还是先前的条件判断指令,例如 while 循环:

1
2
3
4
5
6
7
8
public class Demo1_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}

对应的部分字节码:

 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

再比如 do...while 循环:

1
2
3
4
5
6
7
8
public class Demo1_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}

对应的部分字节码:

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

最后再看看 for 循环:

1
2
3
4
5
6
7
public class Demo1_6 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {

}
}
}

对应的部分字节码:

 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

比较 whilefor 的字节码,会发现它们是一模一样的,殊途也能同归。

2.7 练习 - 判断结果

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

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo1_7 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
// 0
System.out.println(x);
}
}

对应的部分字节码:

 0: iconst_0
 1: istore_1
 2: iconst_0
 3: istore_2
 4: iload_1
 5: bipush        10
 7: if_icmpge     21
10: iload_2
11: iinc          2, 1
14: istore_2
15: iinc          1, 1
18: goto          4
21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
28: return

执行完前 4 步后,局部变量表 slot 1 存放 islot 2 存放 x

加载局部变量表 slot 1 的数据,压入操作数栈,再执行 bipush 1010 也压入操作数栈,使用 if_icmpge 21 比较前者是否大于等于后者,如果是,调到第 21 步,否则依次执行下一步。

不满足 if_icmpge 时,加载 slot 2 的数据(即 x,此时为 0),局部变量表 slot 2 位置的数据(也是 x,但先前已经加载到操作数栈上的 x 不会受影响,操作数栈上的 x 依旧是 0)加一,弹出操作数栈的栈顶元素,写入 slot 2 的位置,也就是让操作数栈上的 0 覆盖先前在局部变量表上加一的 1,这样操作后,局部变量表 slot 2 的数据并没有发生任何变化。

后续继续循环时,slot 2 的数据最终总是 0,因此最终打印 x 时还是 0

2.8 构造方法

<cinit>()V

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

static int i = 10;

static {
i = 20;
}

static {
i = 30;
}
}

编译器会按从上到下的顺序,收集所有 static 静态代码块和静态变量赋值的代码,最终合并成一个特殊的方法 <cinit>()V,对应的部分字节码:

 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

<cinit>()V 方法会在类加载的初始化阶段被调用。

因此上述代码里 i 最终的值为 30

<init>()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
public class Demo1_8_2 {

private String a = "s1";

{
b = 20;
}

private int b = 10;

{
a = "s2";
}

public Demo1_8_2(String a, int b) {
this.a = a;
this.b = b;
}

public static void main(String[] args) {
Demo1_8_2 obj = new Demo1_8_2("s3", 30);
// s3
System.out.println(obj.a);
// 30
System.out.println(obj.b);
}
}

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

对应的部分字节码:

0: aload_0
1: invokespecial #1                   // super."<init>":()V
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                           // <- slot 1(a) "s3"              |
30: putfield      #3                  // -> this.a                      |
33: aload_0                                                             |
34: iload_2                           // <- slot 2(b) 30                |
35: putfield      #4                  // -> this.a  ---------------------
38: return
LineNumberTable: ...
LocalVariableTable:
Start  Length  Slot  Name   Signature
0      39     0  this   Lindi/mofan/Demo1_8_2;
0      39     1     a   Ljava/lang/String;
0      39     2     b   I

2.9 方法调用

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
public class Demo1_9 {
public Demo1_9() {
}

private void test1() {
}

private final void test2() {
}

public void test3() {
}

public static void test4() {
}

public static void main(String[] args) {
Demo1_9 obj = new Demo1_9();
obj.test1();
obj.test2();
obj.test3();
obj.test4();
Demo1_9.test4();
}
}

对应的部分字节码:

 0: new           #2                  // class indi/mofan/Demo1_9
 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

可以发现:

  • 调用构造方法、私有方法、final 方法时,使用 invokespecial 指令
  • 调用公共方法时,使用 invokevirtual 指令
  • 调用静态方法时,使用 invokestatic 指令

其中,invokespecialinvokestatic 属于静态绑定,能够在编译期确定调用的目标方法,性能更高;而 invokevirtual 属于动态绑定,调用的公共方法可能是当前类的、也可能是父类的(方法重写),在运行时才能确定调用的目标方法,性能相对更低。

new 一个对象

1
Demo1_9 obj = new Demo1_9();

上述 Java 代码对应 4 步字节码:

  1. new:在堆空间为需要创建的对象分配内存,分配成功后把对象引用放入操作数栈
  2. dup:复制操作数栈上的栈顶数据(现在操作数栈里有两份相同的引用)
  3. invokespecial:弹出操作数栈的栈顶数据(弹出一份引用)并调用构造方法
  4. astore_1:弹出操作数栈的栈顶数据(弹出另一份引用),并将其存入局部变量表的 slot 1

使用实例对象调用静态方法

可以直接按照 类名.静态方法名() 的形式去调用静态方法,但使用 实例对象.静态方法名() 来调用静态方法也不会编译报错。

那它们在字节码指令层面上有什么区别吗?

20: aload_1
21: pop
22: invokestatic  #7                  // Method test4:()V
25: invokestatic  #7                  // Method test4:()V

20~22 对应使用 实例对象.静态方法名() 的形式来调用静态方法,25 对应使用 类名.静态方法名() 的形式来调用静态方法。

当使用 实例对象.静态方法名() 来调用静态方法时,先执行 aload_1 从将局部变量表中 slot 1 位置的数据加载到操作数栈中,由于调用静态方法并不需要实例对象,因此紧接着执行 pop 弹出操作数栈的栈顶元素,最后再使用 invokestatic 执行静态方法。

因此在日常编码时 推荐使用 实例对象.静态方法名() 的形式来调用静态方法,因为这会多出两条冗余的字节码指令。

2.10 多态的原理

运行测试代码

运行代码时添加 JVM 参数 -XX:-UseCompressedOops -XX:-UseCompressedClassPointers 禁用指针压缩:

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
package indi.mofan;

/**
* 运行时添加以下 JVM 参数,禁用指针压缩:
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*
* @author mofan
* @date 2025/10/5 20:03
*/
public class Demo1_10 {

public static void test(Animal animal) {
animal.eat();
System.out.println(animal);
}

public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}

static abstract class Animal {
public abstract void eat();

@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}

static class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}

static class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
}

在终端使用 jps 命令,查看 Java 进程 id:

29744 Launcher
21364 Jps
12252 Main
26396 HSDB
29772 Demo1_10

目标进程 id 是 29772

使用 HSDB 工具

进入 JDK 安装目录,执行:

1
2
3
4
# jdk 8
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
# jdk 9 及以上
jhsdb hsdb

如果在 Windows 平台上运行时出现「‘.’ 不是内部或外部命令,也不是可运行的程序 或批处理文件。」错误,这是在 Windows 系统上使用了 Linux/Unix 风格的路径分隔符,把 / 改成 \ 即可:

1
java -cp .\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

进入 HSDB - HotSpot Debugger 页面,点击右上角 File,再点击「Attach to HotSpot process…」并输入前面通过 jps 命令得到的进程 id:

Attach-to-HotSpot-process

点击 OK,耐心等待:

HSDB查看JavaThread

点击 HSDB 工具栏的 Tools,再点击「Find Object by Query」:

Find-Object-by-Query

SOQL 的语法与 SQL 类似,比如现在需要查询 Java 进程中 indi.mofan.Demo1_10.Dog 对象:

1
select d from indi.mofan.Demo1_10$Dog d

查询Dog对象

查询后会返回 Dog 对象的内存地址,点击该内存地址:

Dog对象详细信息

前 8 个字节组成 Mark Word,对应上图中的 _mark,其中包含对象的 HashCode、锁标记信息等。

后 8 个字节是对象的类型指针,对象上图中的 _metadata._klass,可以看到该对象的类型是 indi.mofan.Demo1_10$Dog

如果需要查看这两部分信息在内存中的详细信息,点击 HSDB 工具栏的 Windows,再点击「Console」,输入以下命令:

1
mem 0x000001b60bf1d0e8 2

其中 0x000001b60bf1d0e8 是对象地址信息,2 是需要查看几个 Word 信息,这里需要查看全部对象头信息,共两部分,因此键入 2

mem-0x000001b60bf1d0e8

第一个表示 Mark Word 比较简单。第二个表示复制类型指针信息,即 0x000001b492985550

点击 HSDB 工具栏的 Tools,再点击「Inspector」,输入刚刚复制的类型指针信息,然后回车:

Inspector-0x000001b492985550

这就是 Dog 类型在 JVM 虚拟机中的形式。

vtable

多态方法存在于 vtable(虚方法表)中。

vtable 在类结构信息的最后:

vtable-0x000001b492985550

可以看到 vtable 没有具体的地址信息,但可以在类的内存偏移地址上加上 1B8 得到:

1
0x000001b492985550 + 1B8 = 0x000001B492985708

先前对 Dog 对象进行 Inspector 时能够看到 vtable 的长度是 6

vtable长度

打开 Command Line,执行:

1
mem 0x000001B492985708 6

其中:

  • 0x000001B492985708 是计算出的 vtable 内存地址
  • 6vtable 长度

Dog类中的多态方法信息

可以看到 Dog 类的 vtable 里有 6 个动态方法。

点击 HSDB 工具栏的 Tools,再点击「Class Browser」查看类信息,确认 Dog 类中的动态方法是来自于哪:

匹配Dog的eat方法

注意: 如果 Dog 类中的 eat() 方法地址并不在命令行输出的 6 个动态方法中,可以尝试更换 JDK 重试,比如由 Zulu JDK8 更换为 Oracle JDK8,尽量使用 HotSpot JVM。

匹配Animal的toString方法

匹配Object中的四个方法

小结

当执行 invokevirtual 指令时:

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

2.11 异常处理

try-catch

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

对应的部分字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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
      StackMapTable: ...

使用 try-catch 块后,生成的字节码会多出一个 Exception table 结构。其中的 [from, to) 是一个前闭后开的检测范围,比如这里的 [2, 5) 表示检测第 2 行字节码到第 5 行字节码(不包括第 5 行),一旦这个范围的字节码执行时出现异常,再判断异常类型是否与 type 匹配,如果匹配,则跳到 target 对应的字节码行号。

8 行字节码是 astore_2,表示将异常对象 e 的引用存入局部变量表的 slot 2 位置。

多个 single-catch 块的情况

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

对应的部分字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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        30
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        40
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        50
        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
      StackMapTable: ...

多个 catch 块与单个 catch 块类似,只不过由于只能进入 Exception table 中的一个分支,所以局部变量表 slot 2 会被复用。

multi-catch 的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo1_11_3 {
public static void main(String[] args) {
try {
Method test = Demo1_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}

public static void test() {
System.out.println("ok");
}
}

对应的部分字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #2                  // class indi/mofan/Demo1_11_3
         2: ldc           #3                  // String test
         4: iconst_0
         5: anewarray     #4                  // class java/lang/Class
         8: invokevirtual #5                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        11: astore_1
        12: aload_1
        13: aconst_null
        14: iconst_0
        15: anewarray     #6                  // class java/lang/Object
        18: invokevirtual #7                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        21: pop
        22: goto          30
        25: astore_1
        26: aload_1
        27: invokevirtual #11                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
        30: return
      Exception table:
         from    to  target type
             0    22    25   Class java/lang/NoSuchMethodException
             0    22    25   Class java/lang/IllegalAccessException
             0    22    25   Class java/lang/reflect/InvocationTargetException
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12      10     1  test   Ljava/lang/reflect/Method;
           26       4     1     e   Ljava/lang/ReflectiveOperationException;
            0      31     0  args   [Ljava/lang/String;
      StackMapTable: ...

multi-catch 与单个 single-catch、多个 single-catch 类似,只不过在 Exception table 里存在多个 fromtotarget 相同、但 type 不同的异常信息。

finally 块

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

对应的部分字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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    // 剩余的异常类型,比如 Error
            11    15    21   any    // 剩余的异常类型,比如 Error
      LineNumberTable: ...
      LocalVariableTable: ...

根据字节码可知,整个代码分为 3 个分支:

  1. 先执行 try 块的代码,然后执行 finally 块里的代码
  2. 如果执行 try 块里的代码抛出了 Exception,那么会执行 catch 块里的代码,最后再执行 finally 块里的代码
  3. 执行 try 块或 catch 块里的代码时还可能抛出与 Exception 同级的错误,比如 Error,它们无法被捕获,但最后也要执行 finally 块里的代码

综上,finally 块中的代码被复制了 3 份,分别放入 try 流程、catch 流程以及 catch 剩余的异常类型流程。

2.12 练习 - finally 面试题

finally 出现了 return

运行以下代码,控制台会输出什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo1_12_1 {
public static int test() {
try {
return 10;
} finally {
return 20;
}
}

public static void main(String[] args) {
// 20
System.out.println(test());
}
}

对应的部分字节码:

public static int test();
    descriptor: ()I
    flags: (0x0009) 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 放入栈顶
         5: ireturn                // 返回栈顶 int(20)
         6: astore_1               // catch any -> slot 1
         7: bipush        20       // <- 20 放入栈顶
         9: ireturn                // 返回栈顶 int(20)
      Exception table:
         from    to  target type
             0     3     6   any
      LineNumberTable: ...
      StackMapTable: ...
  • 由于 finally 中的 ireturn 被插入到了所有可能的流程中,因此最终返回结果以 finally 中的为准

  • 第 2 行字节码的 istore_0 似乎没什么用,但真的是这样吗?

  • 与先前的例子相比,字节码中没有 athrow 命令了,因此:如果在 finally 中出现了 return,会吞掉异常,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo1_12_1_1 {
    public static void main(String[] args) {
    // 始终返回 20,且没有异常
    System.out.println(test());
    }

    public static int test() {
    try {
    int i = 1 / 0;
    return 10;
    } finally {
    return 20;
    }
    }
    }

finally 对返回值的影响

运行以下代码,控制台又会输出什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo1_12_2 {

public static void main(String[] args) {
// 10
System.out.println(test());
}

public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}

对应的部分字节码:

public static int test();
    descriptor: ()I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         0: bipush        10       // <- 10 放入栈顶
         2: istore_0               // 10 -> i
         3: iload_0                // <- i(10)
         4: istore_1               // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
         5: bipush        20       // <- 20 放回栈顶
         7: istore_0               // 20 -> i
         8: iload_1                // <- slot 1(10) 载入 slot 1 暂存的值
         9: ireturn                // 返回栈顶的 int(10)
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I
      StackMapTable: ...

finally 块中的数据没有被 return,生成的字节码指令中有 athrow 指令,如果出现异常不会被吞。

2.13 synchronized

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

对应的部分字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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                          // lock 引用 -> lock
         8: aload_1                           // <- lock (synchornized 开始)
         9: dup
        10: astore_2                          // lock 引用 -> slot 2
        11: monitorenter                      // monitorenter (lock 引用)
        12: getstatic     #3                  // <- System.out
        15: ldc           #4                  // <- "ok"
        17: invokevirtual #5                  // invokevirtual println:(Ljava/lang/String;)V
        20: aload_2                           // <- slot 2 (lock 引用)
        21: monitorexit                       // monitorexit (lock 引用)
        22: goto          30
        25: astore_3                          // any -> slot 3
        26: aload_2                           // <- slot 2 (lock 引用)
        27: monitorexit                       // monitorexit (lock 引用)
        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;
      StackMapTable: ...

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

3. 编译期处理

所谓 语法糖,指 Java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换了一些代码,主要是为了减轻开发者的负担,算是 Java 编译器给开发者的一个额外「福利」。

注意, 本节代码分析将借助 javap、IDEA 的反编译功能、IDEA 的 jClassLib 插件等工具。编译器的转换结果就是 class 字节码,本节为了便于阅读,将给出几乎等价的 Java 源码,这并不表示编译器还会生成中间 Java 源码。

3.1 默认构造器

1
2
public class Candy1 {
}

编译后生成的字节码等价于:

1
2
3
4
5
6
7
public class Candy1 {
// 这个无参构造器会由编译器自动加上
public Candy1() {
// 调用父类的 Object 的无参构造,即 java/lang/Object."<init>":()V
super();
}
}

3.2 自动拆装箱

在 JDK 5 中添加了自动拆装箱特性:

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

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

1
2
3
4
5
6
public static void main(String[] args) {
// 基本类型 -> 包装类型:装箱
Integer x = Integer.valueOf(1);
// 包装类型 -> 基本类型:拆箱
int y = x.intValue();
}

在 JDK 5 之前,包装类型和基本类型之间的转换需要手动处理(尤其是集合类中操作的都是包装类型),这非常麻烦,JDK 5 引入自动拆装箱后,这些手动处理的代码都可以由编译器在编译阶段完成。

3.3 泛型集合取值

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

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

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

1
2
// 将 Object 转换成 Integer
Integer x = (Integer) list.get(0);

如果前面的 x 变量类型修改为 int 基本类型,编译器还会自动拆箱:

1
2
// 将 Object 转换为 Integer,然后再拆箱
int x = ((Integer) list.get(0)).intValue();

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

 public indi.mofan.candy.Candy3();
    descriptor: ()V
    flags: (0x0001) 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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lindi/mofan/candy/Candy3;
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) 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
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}

public static void parseGenericType() throws Exception {
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("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
}

输出:

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

可以使用上述方式获取方法参数、返回值的泛型信息。

3.4 可变参数

可变参数也是 JDK 5 中添加的新特性。

1
2
3
4
5
6
7
8
9
10
11
public class Candy4 {
public static void foo(String... args) {
// 可变参数可直接赋值给数组
String[] array = args;
System.out.println(Arrays.toString(array));
}

public static void main(String[] args) {
foo("hello", "world");
}
}

可变参数 String... args 的本质是 String[] args,Java 编译器会在编译阶段将上述代码转换为:

1
2
3
4
5
6
7
8
public static void foo(String[] args) {
String[] array = args;
System.out.println(Arrays.toString(array));
}

public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}

注意,如果调用了 foo() 则等价于 foo(new String[]{}),创建了一个空数组,而不是传入 null

3.5 foreach 循环

foreach 循环也是 JDK 5 引入的语法糖:

1
2
3
4
5
6
7
8
9
public class Candy5_1 {
public static void main(String[] args) {
// 数组赋初值的简化写法也是语法糖
int[] array = {1, 2, 3, 4, 5};
for (int x : array) {
System.out.println(x);
}
}
}

编译后生成的字节码等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 x = array[i];
System.out.println(x);
}
}
}

如果是在集合上使用 foreach 循环呢?

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

应用于集合的 foreach 循环会被编译期转换为对迭代器的调用,编译后生成的字节码等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Candy5_2 {

public Candy5_2() {
}

public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Integer x = (Integer) iterator.next();
System.out.println(x);
}
}
}

foreach 循环的写法能够配合数组、实现了 Iterable 接口(提供获取 Iterator 的方式)的集合类一起使用。

3.6 switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
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");
}
}
}
}

编译后生成的字节码等价于:

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 Candy6_1 {
public Candy6_1() {
}

public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 99162322: {
if (str.equals("hello")) {
x = 0;
}
break;
}
case 113318802: {
if (str.equals("world")) {
x = 1;
}
}
}
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}

在编译生成的字节码中执行了两次 switch,第一次根据字符串的 hashCodeequals 将字符串的转换为相应 byte 类型,第二次再利用 byte 进行比较。

为什么第一次时必须既比较 hashCode,又比较 equals 呢?

hashCode 是为了提高效率,减少比较次数;equals 是为了防止哈希冲突。

例如 BMC. 两个字符串的 hashCode 值都是 2123:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Candy6_2 {
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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Candy6_2 {

public Candy6_2() {
}

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

3.7 switch 枚举

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

enum Sex {
MALE, FEMALE
}

编译后生成的字节码等价于:

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

static class $MAP {
static int[] map = new int[2];

static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}

public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}

$MAP 是一个合成类,仅 JVM 使用,开发者不可见,用来映射枚举的 ordinal 与数组元素的关系。枚举的 ordinal 表示枚举对象的序号,从 0 开始,即 MALE.ordinal() = 0FEMALE.ordinal() = 1

3.8 枚举类

JDK 7 新增了枚举类,现有如下枚举:

1
2
3
public enum Sex {
MALE, FEMALE
}

编译后生成的字节码等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
}

3.9 try-with-resources

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

1
2
3
4
5
try (资源变量 = 创建资源变量) {

} catch ( ) {

}

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

1
2
3
4
5
6
7
8
9
public class Candy9 {
public static void main(String[] args) {
try (FileInputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.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
35
36
37
public class Candy9 {

public Candy9() {
}

public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// 编写的代码出现异常
t = e1;
throw e1;
} finally {
// 资源不为空
if (is != null) {
// 编写的代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 调用 close() 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?

为了防止异常信息的丢失。

如果 try-with-resources 生成的 finally 中如果抛出了异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Candy9_2 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}

static class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
throw new Exception("close 异常");
}
}
}

输出:

java.lang.ArithmeticException: / by zero
	at indi.mofan.candy.Candy9_2.main(Candy9_2.java:11)
	Suppressed: java.lang.Exception: close 异常
		at indi.mofan.candy.Candy9_2$MyResource.close(Candy9_2.java:20)
		at indi.mofan.candy.Candy9_2.main(Candy9_2.java:12)

可以看到,两个异常信息都不会丢失。

3.10 方法重写时的桥接方法

方法重写时的返回值有两种情况:

  • 父子类的返回值完全一致

  • 子类返回值可以是父类返回值的子类,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static class A {
    public Number m() {
    return 1;
    }
    }

    static class B extends A {
    @Override
    // 子类 m 方法的返回值是 Integer,是父类 m 方法返回值 Number 的子类
    public Integer m() {
    return 2;
    }
    }

对于子类,Java 编译器会做以下处理:

1
2
3
4
5
6
7
8
9
10
11
static class B extends A {
public Integer m() {
return 2;
}

// 此方法才是真正重写父类的 m 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}

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

1
2
3
4
5
public static void main(String[] args) {
for (Method method : B.class.getDeclaredMethods()) {
System.out.println(method);
}
}

输出:

public java.lang.Integer indi.mofan.candy.Candy10$B.m()
public java.lang.Number indi.mofan.candy.Candy10$B.m()

3.11 匿名内部类

有如下匿名内部类的表现形式:

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

转换后的代码:

1
2
3
4
5
6
7
8
9
final class Candy11$1 implements Runnable {
public Candy11$1() {
}

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

如果匿名内部类里引用了局部变量:

1
2
3
4
5
6
7
8
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok" + x);
}
};
}

转换后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
final class Candy11$1 implements Runnable {
int val$x;

public Candy11$1(int val$x) {
this.val$x = val$x;
}

@Override
public void run() {
System.out.println("ok" + this.val$x);
}
}
1
2
3
4
5
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化,如果变化,那么 val$x 属性没有机会再跟着一起变化。

4. 类加载阶段

4.1 加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 Java 类,其中重要的 field 有:

    • _java_mirror Java 的类镜像,例如对 String 来说,就是 String.class,作用是把 Klass 暴露给Java 使用

    • _super 父类

    • _fields 成员变量

    • _methods 方法

    • _constants 常量池

    • _class_loader 类加载器

    • _vtable 虚方法表

    • _itable 接口方法表

  • 如果这个类还有父类没加载,则先加载父类

  • 加载和链接可能是交替进行的

注意:

  • instanceKlass 这样的「元数据」是存储在方法区的(1.8 后的元空间内),但 _java_mirror 是存储在堆中的
  • 可以通过先前介绍的 HSDB 工具进行查看

instanceKlass

4.2 链接

验证

验证类是否符合 JVM 规范,进行安全性检查。例如使用 UE 等支持二进制数据显示的编辑器修改一个 class 文件的魔数,执行 java 命令时会出现 ClassFormatError 错误。

准备:为 static 变量分配空间,设置默认值:

  • static 变量在 JDK7 之前存储于 instanceKlass 末尾,从 JDK7 开始,存储于 _java_mirror 末尾
  • static 变量的分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值 通常 在初始化阶段完成
  • 如果 static 变量是 final 的基本类型和字符串字面量,其值在编译阶段就确定了,因此这类变量的赋值会在准备阶段完成
  • 如果 static 变量是 final 的引用类型,赋值就是在初始化阶段完成

解析

将常量池中的符号引用解析为直接引用。

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
package indi.mofan.load;


import java.io.IOException;

/**
* @author mofan
* @date 2026/1/1 21:16
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader loader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> cClazz = loader.loadClass("indi.mofan.load.C");

// new C();
System.in.read();
}
}

class C {
D d = new D();
}

class D {
}

运行上述程序后,执行 jps 命令查看 Java 进程 id:

1
2
3
4
592 Main
27812 Launcher
30424 Load2
27948 Jps

进入 JDK 安装目录,使用 CMD 执行以下命令打开 HSDB:

1
.\bin\java.exe -cp .\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

按照前文中的方式,在「Attach to HotSpot process…」窗口里输入 Java 进程 id,即 30424

点击 HSDB 中 Tools 菜单下的「Class Browser」,查看对应 Java 进行中有哪些类信息:

  • 有类 C,查看其常量池信息:

    HSDB下查看C类常量池信息

  • 但没有类 D

    HSDB下查找D类信息

更改先前的示例程序,注释 loadClass() 方法,启用 new C();

1
2
3
4
5
6
7
8
9
10
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader loader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
// Class<?> cClazz = loader.loadClass("indi.mofan.load.C");

new C();
System.in.read();
}
}

再次查看 C 类的常量池信息,此时可以看到 D 类的信息:

HSDB下再次查看C类常量池信息

4.3 初始化

<cinit>()V 方法

初始化,即调用 <cinit>()V 方法(初始化静态变量和静态代码块),虚拟机会保证这个类的「构造方法」的线程安全。

发生的时机

概括地说,类初始化是「懒惰的」:

  • main() 方法所在的类,总被优先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,触发父类的初始化,且在子类初始化之前进行初始化
  • 子类访问父类静态变量,只触发父类的初始化,子类不会被初始化
  • 调用 Class.forName()
  • 使用 new 关键词

不会导致类初始化的情况:

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 使用 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 调用类加载器的 loadClass() 方法加载一个类
  • 调用 Class.forName() 方法,且第二个参数值为 false
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
public class Load3 {
static {
System.out.println("main init");
}

public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量不会触发初始化
// System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
// System.out.println(Arrays.toString(new B[0]));
// 4. 不初始化类 B,但会加载 B、A
// ClassLoader c1 = Thread.currentThread().getContextClassLoader();
// c1.loadClass("indi.mofan.load.B");
// 5. 不初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("indi.mofan.load.B", false, c2);


// 1. 首次访问这个类的静态变量或静态方法时会触发初始化
// System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,触发父类的初始化,且在子类初始化之前进行初始化
// System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类的初始化,子类不会被初始化
// System.out.println(B.a);
// 4. 调用 Class.forName() 方法
Class.forName("indi.mofan.load.B");
}
}

class A {
static int a = 0;

static {
System.out.println("a init");
}
}

class B extends A {
final static double b = 5.0;
static boolean c = false;

static {
System.out.println("b init");
}
}

5. 类加载器

以 JDK8 为例:

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

5.1 启动类加载器

使用 Bootstrap 类加载器加载类:

1
2
3
4
5
public class F {
static {
System.out.println("bootstrap F init");
}
}

执行:

1
2
3
4
5
6
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("indi.mofan.load.F");
System.out.println(clazz.getClassLoader());
}
}

执行上述代码时不能在 IDEA 内进行,需要使用命令行进入项目的输出目录,在 Maven 项目下就是所在模块的 \target\classes 目录。

使用以下命令执行:

1
.\target\classes> java -Xbootclasspath/a:. indi.mofan.load.Load5_1

输出:

bootstrap F init
null

输出的类加载器信息为 null,这表明使用了 Bootstrap 类加载器进行了类加载。

-Xbootclasspath/a:. 的解析

-Xbootclasspath 表示设置 bootclasspath。

其中 /a:. 表示将当前目录追加至 bootclasspath 之后。

可以用这个方法替换掉一些核心类:

  • java -Xbootclasspath:<new bootclasspath>:使用新路径替换 JAVA_HOME/jre/lib 目录
  • java -Xbootclasspath/a:<追加路径>:进行后追加
  • java -Xbootclasspath/p:<追加路径>:进行前追加

5.2 扩展类加载器

新建 G 类:

1
2
3
4
5
public class G {
static {
System.out.println("ext G init");
}
}

编译为 class 文件后,使用以下命令将其打包成 jar 文件:

1
jar -cvf my.jar indi\mofan\load\G.class

将打包得到的 my.jar 文件放入使用的 JDK 的 /jre/lib/ext 目录下。

修改 G 类,使得 /jre/lib/ext 目录下和当前项目目录下都存在一个 G 类:

1
2
3
4
5
6
public class G {
static {
// System.out.println("ext G init");
System.out.println("classpath G init");
}
}

测试下:

1
2
3
4
5
6
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("indi.mofan.load.G");
System.out.println(clazz.getClassLoader());
}
}

输出:

ext G init
sun.misc.Launcher$ExtClassLoader@379619aa

根据输出信息可以看到,使用拓展类加载器加载了拓展类路径下的 G 类,而非应用程序类路径下的 G 类。

5.3 双亲委派模型

参考 注解、类的加载、反射,不再赘述。

5.4 线程上下文类加载器

我们在使用 JDBC 时需要加载 Driver 驱动,但就算没有以下代码也能够让 com.mysql.jdbc.Driver 正确加载,这是为什么呢?

1
Class.forName("com.mysql.jdbc.Driver");

这与 Java 的 SPI 机制有关,参考 Java 的类资源加载,不再赘述。

而所谓的「线程上下文类加载器」,即使用 Thread.currentThread().getContextClassLoader() 方法获取到的类加载器。在 Java 线程启动时,JVM 默认会将应用程序类加载器设置到该线程的上下文类加载器中。

5.5 自定义类加载器

参考 mofan-demo/classloader,不再赘述。

6. 运行时优化

6.1 即时编译

分层编译(TieredCompilation)

有以下代码:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n", i, end - start);
}
}

运行程序后在控制台上能发现随着外部循环的执行,内部循环耗费的时间越来越少。

原因是什么呢?

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)

  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)

  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)

  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)

  • 4 层,使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如「方法的调用次数」,「循环的回边次数」等。

即时编译器(JIT)与解释器的区别:

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释

  • JIT 是将一些字节码编译为字节码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译

  • 解释器是将字节码解释为针对所有平台都通用的机器码

  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用代码,无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;对于仅占据小部分的热点代码,则可以将其编译成机器码,以达到理想的运行速度。在执行效率上进行简单的比较有 Interpreter < C1 < C2,总体目标是发现热点代码(这也是 HotSpot 名称的由来),然后进行优化。

先前示例代码中使用了一种名为「逃逸分析」的优化手段,用于判断新建的对象是否「逃逸」。以这段示例代码为例,简单来说就是判断内部循环新建的对象是否被外部循环使用,然后发现并没有被使用,那干脆就不创建这个对象了,于是随着外层循环的执行,耗费的时间越来越少,甚至出现数量级上的差距。

可以使用 JVM 参数 -XX:-DoEscapeAnalysis 来关闭逃逸分析。

方法内联(Inlining)

有以下代码:

1
2
3
private static int square(final int i) {
return i * i;
}
1
System.out.println(square(9));

如果发现 square() 是热点代码,并且长度不太长,就会进行内联。所谓内联指的是将方法内的代码拷贝、粘贴到调用者的位置:

1
System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化:

1
System.out.println(81);

看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JIT2 {
private static int square(final int i) {
return i * i;
}

public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%d\t%d\t%d\n", i, x, end - start);
}
}
}

随着外层循环的执行,耗时居然变成了 0,这是因为进行了「方法内联」和「常量折叠」的优化。

相关 JVM 参数:

  • -XX:+UnlockDiagnosticVMOptions:解锁一些诊断性的虚拟机选项(某些高级和诊断性功能是默认禁用的),仅在开发调试使用,不能在生产中使用,会导致 JVM 行为不稳定,甚至影响系统安全性和可靠性。
  • -XX:+PrintInlining:在 JIT 编译过程中打印方法内联的详细信息,需要与 -XX:+UnlockDiagnosticVMOptions 一起使用,否则无法使用该参数。
  • -XX:+PrintCompilation:打印 JIT 编译的时间、方法、编译层级等信息。
  • -XX:CompileCommand=dontinline,*JIT2.square:禁用任意包下 JIT2 类中 square() 方法的方法内联优化

字段优化

本节需要使用到 JMH,参考 JMH

有以下代码:

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
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
volatile int[] elements = randomInts(1_000);

private static int[] randomInts(int size) {
ThreadLocalRandom random = ThreadLocalRandom.current();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = random.nextInt();
}
return result;
}

@Benchmark
public void test1() {
for (int i = 0; i < elements.length; i++) {
doSum(elements[i]);
}
}

@Benchmark
public void test2() {
int[] local = this.elements;
for (int i = 0; i < local.length; i++) {
doSum(local[i]);
}
}

@Benchmark
public void test3() {
for (int element : this.elements) {
doSum(element);
}
}

static int sum = 0;

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(Benchmark1.class.getSimpleName())
.forks(1)
.build();

new Runner(opt).run();
}
}

doSum() 方法未开启方法内联,输出的报告如下:

Benchmark          Mode  Cnt       Score       Error  Units
Benchmark1.test1  thrpt    5  693704.366 ± 18996.437  ops/s
Benchmark1.test2  thrpt    5  756633.830 ± 23192.219  ops/s
Benchmark1.test3  thrpt    5  754530.966 ± 22909.926  ops/s

可以看到,test1() 方法的得分稍低一些。

在上述示例中,doSum() 方法是否内联会影响 elements 成员变量读取的优化。

如果 doSum() 方法内联了,test1() 方法会被优化成类似下面的样子(伪代码):

1
2
3
4
5
6
7
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次求长度不再访问成员变量 <- local
doSum(elements[i]); // 1000 次取下标 i 的元素不再访问成员变量 <- local
}
}

可以节省 1999 次访问成员变量 elements 的操作,但如果 doSum() 方法没有进行内联,则不会进行上述优化。

6.2 反射优化

本节需要使用到 Arthas,参考 Arthas 快速入门

存在以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}

public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
// 17 次反射调用
for (int i = 1; i <= 17; i++) {
long start = System.nanoTime();
// System.out.printf("%d\t", i);
foo.invoke(null);
System.out.printf("%d\t%d\n", i, System.nanoTime() - start);
}
// System.in.read();
}
}

共进行了 17 次反射调用。

观察耗时有:随着调用次数的进行,耗时减少,但在第 16 次的调用中,耗时陡增,第 17 次调用的耗时又恢复正常。

反射调用方法时,如果超过阈值(15),会在运行时生成方法访问器(MethodAccessorImpl 的实现类,可通过 Arthas 的 jad 命令查看),内部将反射的动态调用改为直接调用。

阈值由 ReflectionFactory.inflationThreshold() 方法返回结果控制,默认 15。

通过查看 ReflectionFactory 源码可知:

  • 可以通过设置系统属性 sun.reflect.noInflationtrue 来禁用膨胀,以直接生成 MethodAccessorImpl 的实现类,但首次生成会比较耗时(这也是上述示例代码中第 16 次反射调用的耗时陡增的原因),如果仅反射调用一次,那不划算
  • 可以通过设置系统属性 sun.reflect.inflationThreshold 的值来修改膨胀阈值