JVM 发生 GC 导致线程卡死问题

案例出自 马士兵MAC课程

案例代码

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
public class OSRDemo {
static long counter;

public static void main(String[] args) throws Exception {
System.out.println("main start");
startBusinessThread();
startProblemThread();
// 等待线程启动执行
Thread.sleep(500);
// 执行GC
System.gc();
System.out.println("main end");
}

private static void startProblemThread() {
new Thread(() -> {
System.out.println("Problem start");
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 100000; j++) {
counter += i % 33;
counter += i % 333;
}
}
}).start();
}

private static void startBusinessThread() {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
System.out.println("执行业务一");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread-01").start();
}
}

案例现象

  1. 主线程创建两个线程,其中一个线程执行业务逻辑,每秒进行一次打印;另一个线程执行循环运算
  2. 启动线程后,JVM 进行 GC 垃圾回收,此时可以发现业务线程和主线程均被阻塞

image-20210920161351031

分析原因

  1. JVM 进行 GC 时,线程需要中断,因此所有线程都需要‘跑’到线程安全点(save point)后,再停顿下来。
  2. 在 JDK1.8 及之前版本,C2 编辑器认为 int 的循环为有限循环,因此 startProblemThread() 创建的线程将不会进入线程安全点
  3. 主线程和业务线程则进入线程安全点阻塞等待,而 startProblemThread() 创建的线程并不会发生中断,导致 GC 没法完成
  4. 因此主线程和业务线程一直阻塞

解决措施

将 JDK 版本换成 11

image-20210920163640802

重新运行后发现不会阻塞

image-20210920163732266

将 int 循环改为 long 值循环

因为 long 值循环意味着这是一个较大的循环,JVM 会进行线程安全点的检查

image-20210920163954285

image-20210920163920287

在循环中加入方法调用

线程安全点的检查发生在方法调用前

image-20210920164649165

注意:如果调用的方法是空方法或者简单循环,则编译器会将其优化,不会继续线程安全点的检测,因此还是会阻塞

用 volatile 修饰 counter

volatile 修饰的代码不会进行优化

添加 JVM 参数 -XX:-UseOnStackReplacement

关闭 OnStackReplacement 优化

添加 JVM 参数 -XX:TieredStopAtLevel=3

不使用 C2 编译器的优化