【深入理解JVM】(五) JVM调优实战--99%的面试官不知道的原理

本文通过一个Java代码示例探讨了GC长时间暂停的问题,发现与volatile关键字和栈上替换(OnStackReplacement)优化有关。当使用volatile修饰变量时,GC耗时显著减少,而启用-XX:-UseOnStackReplacement参数也改善了性能。问题源自C2编译器的激进优化,可能导致线程在安全点等待过久。案例源于生产环境的一个加密算法,强调了优化的重要性。
摘要由CSDN通过智能技术生成

不废话上代码

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编译器在编译优化上的激进

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值