不废话上代码
public class Test {
static long counter;
public static void main(String[] args) throws Exception {
System.out.println("main start");
startBusinessThread();
startProblemThread();
//等待线程启动
Thread.sleep(500);
// 执行gc
long gcStartTime = System.currentTimeMillis();
System.gc();
System.out.println("GC花费时间: "+ (System.currentTimeMillis()-gcStartTime) + "毫秒");
System.out.println("main end");
}
public static void startProblemThread(){
new Thread(new MyRun()).start();
}
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("problem start");
for (int i = 0; i < 100000000; i++) {
for (int j = 0; j < 1000; j++) {
counter += i % 33;
counter += i % 333;
}
// method();
}
System.out.println("problem end");
}
}
public static void method(){
for (int i = 0; i < 5; i++) {
i ++;
}
}
public static void startBusinessThread(){
new Thread(()->{
System.out.println("业务线程-1 start");
for (;;){
System.out.println("执行业务1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
System.out.println("业务线程-2 start");
for (;;){
try {
System.out.println("执行业务2");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
机器配置:CPU:Inter core - i9-9900KF
JDK:8
测试结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
problem end
GC花费时间: 42292毫秒
main end
执行业务1
执行业务2
那么问题来了,为什么会GC这么久?
我们把method()方法放开试试
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("problem start");
for (int i = 0; i < 100000000; i++) {
for (int j = 0; j < 1000; j++) {
counter += i % 33;
counter += i % 333;
}
method();
}
System.out.println("problem end");
}
}
执行结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
method执行了
GC花费时间: 6毫秒
main end
执行业务1
执行业务2
method执行了
method执行了
思考:为什么这次GC时间只有6毫秒?与放开method()方法有关系?
那我们这次把method()方法改造一下:
public static void method(){
int i = 0;
i++;
}
执行结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
problem end
GC花费时间: 42263毫秒
执行业务2
main end
执行业务1
执行业务2
又开始长时间GC
那么问题到底在哪呢?
不妨把counter加上volatile 试试
static volatile long counter;
执行结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
GC花费时间: 7毫秒
main end
执行业务2
执行业务1
又好了
这里我们就会想,volatile 会有什么作用?
volatile 作用有2点:
1.内存可见
2.禁止指令重排序
那么显然这里跟内存可见性没什么关系,肯定跟禁止指令重排序相关,那么到底是不是呢?
method()方法好像证明了跟volatile 也没有直接的关系
这里就引出了volatile 的实现原理,我们暂且不论,先看下面这个例子:
把MyRun 中的int i改成long
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("problem start");
for (long i = 0; i < 100000000; i++) {
for (int j = 0; j < 1000; j++) {
counter += i % 33;
counter += i % 333;
}
// method();
}
System.out.println("problem end");
}
执行结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
GC花费时间: 8毫秒
main end
执行业务2
执行业务1
又可以了
所以我们大致可以猜到,应该是跟编译器有关
这次把代码还原,启用栈上编译,启动前加上参数:
-XX:-UseOnStackReplacement
运行结果:
main start
业务线程-1 start
执行业务1
业务线程-2 start
执行业务2
problem start
GC花费时间: 7毫秒
main end
执行业务1
执行业务2
果然是编译器问题
其实这个案例来源于一个生产问题:
static class MyRun implements Runnable {
//这里其实模拟的是一个加密算法
@Override
public void run() {
System.out.println("problem start");
for (int i = 0; i < 100000000; i++) {
for (int j = 0; j < 1000; j++) {
counter += i % 33;
counter += i % 333;
}
}
System.out.println("problem end");
}
}
GC时,所有的业务线程会到达安全点(safe point),等待GC线程完毕后再继续执行,但是上面这段代码,编译器会认为int i 这个值不会很大,所以在编译时会觉得它不会执行很久,所以这个线程在处理任务时不会在安全点等待,而是等执行完这段逻辑之后才会进入安全点,实际上,GC的时间大概在7毫秒左右,但是等待这段代码执行完到达安全点,花费了42秒.跟垃圾回收器回收垃圾耗时并没有直接关系.
当然,聪明的朋友看到这里,马上就能想到,启用与工作线程并行的垃圾回收器不也可以吗?至于可不可以,有兴趣可以试试.
这个问题的根本原因在于:
C2编译器在编译优化上的激进