深入拆解Java虚拟机

生成字节码

*.java –> *.jasm –> *.class

字节码结构

常量池

方法描述

字节码工具

  • javac
    将java文件编译为class

  • javap
    将class反编译为jasm

  • asmtools
    将jasm编译为class;将class反编译为jasm

  • cfr http://www.benf.org/other/cfr/
    将class反编译为java

jvm如何加载字节码

加载 –> 链接 –> 初始化

  • 类加载器
    双亲委派

  • 验证、准备、解析
    绕过java语言规范
    绑定变量及方法表

  • 类的初始化
    单例

jvm如何进行方法调用

一个类的多个方法(不管重载还是重写),生成不同的方法描述

Demo.java

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

public void doTest(String id){

}

public void doTest(String id, String id2){

}

public void doTest(int id){

}

}

jvm靠字节码中的方法描述来确定方法(不同的方法名 或 相同方法名但不同参数 都生成了不同的方法描述)

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
lixl@DESKTOP-0SMOHUS MINGW64 /f/test
$ javac Demo.java

lixl@DESKTOP-0SMOHUS MINGW64 /f/test
$ javap -v Demo.class
Classfile /F:/test/Demo.class
Last modified 2023-11-5; size 381 bytes
MD5 checksum ba38988fb81cc60e84535dff11ce4cac
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // Demo
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 doTest
#9 = Utf8 (Ljava/lang/String;)V
#10 = Utf8 (Ljava/lang/String;Ljava/lang/String;)V
#11 = Utf8 (I)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 Demo
#16 = Utf8 java/lang/Object
{
public Demo();
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 void doTest(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 5: 0

public void doTest(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 9: 0

public void doTest(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 13: 0
}
SourceFile: "Demo.java"

调用的五个指令

  • 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
    -- Customer.java

    interface Customer {
    boolean isVIP();
    }

    -- Merchant.java

    class Merchant {
    public Number actionPrice(double price, Customer customer) {
    invokeStaticMethod1();// invokestatic
    invokePrivateMethod1();// invokespecial
    invokePublicMethod1();// invokevirtual
    boolean flag = customer.isVIP();// invokeinterface

    return null;
    }

    private static void invokeStaticMethod1(){

    }

    private void invokePrivateMethod1(){

    }

    public void invokePublicMethod1(){

    }

    }
  • jasm

    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
    $ javap -v Merchant.class
    Classfile /F:/test/Merchant.class
    Last modified 2023-11-6; size 565 bytes
    MD5 checksum 84ac0cd5304df3835e102360b8325099
    Compiled from "Merchant.java"
    class Merchant
    minor version: 0
    major version: 52
    flags: ACC_SUPER
    Constant pool:
    #1 = Methodref #7.#19 // java/lang/Object."<init>":()V
    #2 = Methodref #6.#20 // Merchant.invokeStaticMethod1:()V
    #3 = Methodref #6.#21 // Merchant.invokePrivateMethod1:()V
    #4 = Methodref #6.#22 // Merchant.invokePublicMethod1:()V
    #5 = InterfaceMethodref #23.#24 // Customer.isVIP:()Z
    #6 = Class #25 // Merchant
    #7 = Class #26 // java/lang/Object
    #8 = Utf8 <init>
    #9 = Utf8 ()V
    #10 = Utf8 Code
    #11 = Utf8 LineNumberTable
    #12 = Utf8 actionPrice
    #13 = Utf8 (DLCustomer;)Ljava/lang/Number;
    #14 = Utf8 invokeStaticMethod1
    #15 = Utf8 invokePrivateMethod1
    #16 = Utf8 invokePublicMethod1
    #17 = Utf8 SourceFile
    #18 = Utf8 Merchant.java
    #19 = NameAndType #8:#9 // "<init>":()V
    #20 = NameAndType #14:#9 // invokeStaticMethod1:()V
    #21 = NameAndType #15:#9 // invokePrivateMethod1:()V
    #22 = NameAndType #16:#9 // invokePublicMethod1:()V
    #23 = Class #27 // Customer
    #24 = NameAndType #28:#29 // isVIP:()Z
    #25 = Utf8 Merchant
    #26 = Utf8 java/lang/Object
    #27 = Utf8 Customer
    #28 = Utf8 isVIP
    #29 = Utf8 ()Z
    {
    Merchant();
    descriptor: ()V
    flags:
    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 java.lang.Number actionPrice(double, Customer);
    descriptor: (DLCustomer;)Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=5, args_size=3
    0: invokestatic #2 // Method invokeStaticMethod1:()V
    3: aload_0
    4: invokespecial #3 // Method invokePrivateMethod1:()V
    7: aload_0
    8: invokevirtual #4 // Method invokePublicMethod1:()V
    11: aload_3
    12: invokeinterface #5, 1 // InterfaceMethod Customer.isVIP:()Z
    17: istore 4
    19: aconst_null
    20: areturn
    LineNumberTable:
    line 3: 0
    line 4: 3
    line 5: 7
    line 6: 11
    line 8: 19

    public void invokePublicMethod1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=0, locals=1, args_size=1
    0: return
    LineNumberTable:
    line 21: 0
    }
    SourceFile: "Merchant.java"
  • jasm 方法调用指令

invokestatic 用于调用静态方法。

invokespecial 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。

invokevirtual 用于调用非私有实例方法。

invokeinterface 用于调用接口方法。

invokedynamic 用于调用动态方法。

1
2
3
4
5
6
可以被子类覆盖的方法。
例如,某段程序调用父类的方法A.foo(),由于调用者(receiver)是子类B的实例,
实际执行的是子类的同名同参数方法B.foo()。
那么A.foo就是一个虚方法,因为你不知道会调到哪里去

这是面向对象编程的一个重要概念,用来实现多态的

jvm方法表

类加载机制的链接部分中,类加载的准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。

方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。

这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。
方法表满足两个特质:
其一,子类方法表中包含父类方法表中的所有方法;
其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

  • 动态绑定与静态绑定
    方法调用指令中的符号引用会在执行之前解析成实际引用。
    对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。
    对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。
    在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

  • 内联缓存
    是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。
    在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。
    如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。

JVM是如何处理异常的

异常处理的两大组成要素是抛出异常和捕获异常

抛出异常可分为显式和隐式两种。
显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。
隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。
举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)

在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例;
Throwable –> Error Exception
Exception –> RuntimeException

  • jvm如何捕获异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) {
    try {
    mayThrowException();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    // 对应的Java字节码
    public static void main(java.lang.String[]);
    Code:
    0: invokestatic mayThrowException:()V
    3: goto 11
    6: astore_1
    7: aload_1
    8: invokevirtual java.lang.Exception.printStackTrace
    11: return
    Exception table:
    from to target type
    0 3 6 Class java/lang/Exception // 异常表条目

编译过后,该方法的异常表拥有一个条目。
其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3)。
该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始。
条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception。

  • 带finally的异常

Demo.java

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

public static void main(String[] args) {
System.out.println(1);
try {
System.out.println(2);
int i = 10/0;
} catch (Exception e) {
// TODO: handle exception
System.out.println(3);
}finally{
System.out.println(4);
}
System.out.println(5);
}

}

jasm

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
$ javap -v Demo.class
Classfile /F:/test/Demo.class
Last modified 2023-11-6; size 589 bytes
MD5 checksum d14eedd96263de4ef43711cb0dc24b78
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #21.#22 // java/io/PrintStream.println:(I)V
#4 = Class #23 // java/lang/Exception
#5 = Class #24 // Demo
#6 = Class #25 // 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 StackMapTable
#14 = Class #23 // java/lang/Exception
#15 = Class #26 // java/lang/Throwable
#16 = Utf8 SourceFile
#17 = Utf8 Demo.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Class #27 // java/lang/System
#20 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#21 = Class #30 // java/io/PrintStream
#22 = NameAndType #31:#32 // println:(I)V
#23 = Utf8 java/lang/Exception
#24 = Utf8 Demo
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/Throwable
#27 = Utf8 java/lang/System
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Utf8 java/io/PrintStream
#31 = Utf8 println
#32 = Utf8 (I)V
{
public Demo();
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=3, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iconst_2
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: bipush 10
16: iconst_0
17: idiv
18: istore_1
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
22: iconst_4
23: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
26: goto 57
29: astore_1
30: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
33: iconst_3
34: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
37: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
40: iconst_4
41: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
44: goto 57
47: astore_2
48: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
51: iconst_4
52: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
55: aload_2
56: athrow
57: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
60: iconst_5
61: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
64: return
Exception table:
from to target type
7 19 29 Class java/lang/Exception
7 19 47 any
29 37 47 any
LineNumberTable:
line 4: 0
line 6: 7
line 7: 14
line 12: 19
line 13: 26
line 8: 29
line 10: 30
line 12: 37
line 13: 44
line 12: 47
line 13: 55
line 14: 57
line 15: 64
StackMapTable: number_of_entries = 3
frame_type = 93 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 81 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 9 /* same */
}
SourceFile: "Demo.java"

可以看到,编译结果包含三份 finally 代码块。
其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。
最后一份则作为异常处理器,监控 try 代码块以及 catch 代码块。
它将捕获 try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常。

等效于

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

public static void main(String[] args) {
System.out.println(1);
try {
System.out.println(2);
int i = 10/0;
System.out.println(4);//finally{}
} catch (Exception e) {
// TODO: handle exception
System.out.println(3);
System.out.println(4);//finally{}
}
System.out.println(5);
}

}

JVM是如何实现反射的

实现方式

查阅 Method.invoke 的源代码,那么你会发现,它实际上委派给 MethodAccessor 来处理。
MethodAccessor 是一个接口,它有两个已有的具体实现:
一个通过本地方法来实现反射调用,另一个则使用了委派模式。

  • 本地实现
    类加载到虚拟机之后,其方法都加载到方法区,可以明确的指向一个内存地址。
    反射调用无非就是将传入的参数准备好,然后调用进入目标方法。

  • 动态实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // v0版本
    import java.lang.reflect.Method;

    public class Test {
    public static void target(int i) {
    new Exception("#" + i).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
    Class<?> klass = Class.forName("Test");
    Method method = klass.getMethod("target", int.class);
    method.invoke(null, 0);
    }
    }

    # 不同版本的输出略有不同,这里我使用了Java 10。
    $ java Test
    java.lang.Exception: #0
    at Test.target(Test.java:5)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
    a t java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
    t java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
    java.base/java.lang.reflect.Method.invoke(Method.java:564)
    t Test.main(Test.java:131

反射调用先是调用了 Method.invoke,然后进入委派实现(DelegatingMethodAccessorImpl),
再然后进入本地实现(NativeMethodAccessorImpl),最后到达目标方法。

这里你可能会疑问,为什么反射调用还要采取委派实现作为中间层?直接交给本地实现不可以么?
其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),
直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。

动态实现和本地实现相比,其运行效率要快上 20 倍 。
这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,
但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍 。

开销大的原因

在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。
在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。
这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。

方法的反射调用会带来不少性能开销,原因主要有三个:
变长参数方法导致的 Object 数组,
基本类型的自动装箱、拆箱,
还有最重要的方法内联。

java对象的内存布局

  • 对象头

每个对象都有一个对象头,对象头包括两部分,标记信息和类型指针。

标记信息包括哈希值,锁信息,GC信息。类型指针指向这个对象的class。

两个信息分别占用8个字节,所以每个对象的额外内存为16个字节。很消耗内存。

  • 压缩指针

为了减少类型指针的内存占用,将64位指针压缩至32位,进而节约内存。之前64位寻址,寻的是字节。现在32位寻址,寻的是变量。再加上内存对齐(补齐为8的倍数),可以每次寻变量都以一定的规则寻找,并且一定可以找得到。

  • 内存对齐

内存对齐的另一个好处是,使得CPU缓存行可以更好的实施。保证每个变量都只出现在一条缓存行中,不会出现跨行缓存。提高程序的执行效率。

  • 字段重排序

其实就是更好的执行内存对齐标准,会调整字段在内存中的分布,达到方便寻址和节省空间的目的。

  • 虚共享

当两个线程分别访问一个对象中的不同volatile字段,理论上是不涉及变量共享和同步要求的。但是如果两个volatile字段处于同一个CPU缓存行中,对其中一个volatile字段的写操作,会导致整个缓存行的写回和读取操作,进而影响到了另一个volatile变量,也就是实际上的共享问题。

  • @Contented注解

该注解就是用来解决虚共享问题的,被该注解标识的变量,会独占一个CPU缓存行。但也因此浪费了大量的内存空间。

垃圾回收

  • 引用计数法与可达性分析
    引用计数法:每个对象都有一个引用计数器,被引用一次+1;等于0就都是垃圾。
    会有循环引用的问题
    可达性分析:从GC-root开始标记被引用的对象,没被标记的就被作为垃圾。
    误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。

  • Stop-the-world 以及安全点
    在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,
    那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。
    这也就造成了垃圾回收所谓的暂停时间(GC pause)。

Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。
当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,
才允许请求 Stop-the-world 的线程进行独占的工作。

  • 垃圾回收的三种方式
    清除:对象新建时记录分配地址,不用的对象加入空闲列表,只清理空闲列表内的对象即可;
    会有内存碎片,导致后续大的对象无法被创建。
    压缩:把存储的对象压缩放置到连续空间;
    但压缩算法本身比较复杂。
    复制:把内存分成两份,一半用来创建对象,当这一半内存快满时复制到另一半内存;
    比较浪费内存

  • 分代模型(新生代(Eden/Surviver)和老年代 )
    新生代:新生对象,生存时间短;复制-清理,执行速度快,stw时间短;8:1:1
    老年代:对象生成时间长;标记清理

    问题:
    1、TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)
    解决多个线程同时分配段内存的情况;预先分配,免得冲突
    2.Eden/Surviver比例可以调整;Surviver复制次数及Eden占用比例可以调整;
    3.卡表(Card Table):为了解决老年代回收垃圾(Minor GC)是需要进行全堆扫描的情况,
    Hotspot将整个堆分为一个个512字节大小的卡,卡表内记录每张卡的使用状态。
    写屏障(write barrier)、虚共享(false sharing)

  • jvm中的垃圾回收器
    针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New (标记 - 复制算法)

针对老年代的垃圾回收器也有三个:刚刚提到的 Serial Old 和 Parallel Old(标记 - 压缩算法),
以及 CMS(标记 - 清除算法,并发)。

G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。

Java 11 引入了 ZGC https://www.zhihu.com/question/287945354/answer/458761494