JVM 运行期优化

一、即时编译

1.1 分层编译

1.1.1 现象

        先执行下面的代码,代码很简单,只是记录下 200 次创建 1000 个对象耗费的时间

public class JIT1 {
    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));
        }
    }
}

        每次打印的时间如下所示 

        可以看到,前期打印的时间比较长,而后期打印所花费的时间比较短,为什么会出现这种情况呢?

1.1.2 解释

        其实在运行期间,Java 虚拟机会对我们的代码进行一些优化。上面出现这种现象的原因 JVM 将执行状态分成了 5 个层次,如下

        1、0 层,解释器执行(Interpreter),当字节码被反复调用,当反复调用的次数达到阈值时,就会用编译器对字节码进行编译执行,即从 0 层上升到 1 层。

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

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

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

        5、4 层,使用 C2 即时编译器编译执行。

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

        即使编译器分为 C1 C2 两种,C1 只是做了一些基本的优化,C2 做了一些更完全更彻底的优化。

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

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

        2、JIT 是将一些反复执行的代码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译。

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

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

1.1.4 总结

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

        刚才的一种优化手段称之为【逃逸分析】,它会分析你新创建的 new Object() 对象会不会被循环外的代码用到,发现不会,即不会发生逃逸,逃逸分析是发生在 C2 编译器里面的优化,它就会把你创建对象的字节码给替换掉,即不会创建对象了,所以速度一下子提升了很多。

         可以使用  -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果,就会发现时间不会缩短很多了。

1.2 方法内联

        先看下下面的代码

private static int square(final int i) {
	return i * i;
}

System.out.println(square(9));

        JVM 如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置,如下

System.out.println(9 * 9);

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

System.out.println(81);

        接下来我们试验下 JVM 是否存在这种优化,运行下面的代码

public class JIT2 {
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息
    // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
    // -XX:+PrintCompilation 打印编译信息
    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));
        }
    }
    private static int square(final int i) {
        return i * i;
    }
}

        可以看到,当打印时间为 0 时就进行了方法内联,此时根本没有发生方法的调用,可以通过下面的这些参数对内联情况进行控制,如下

# (解锁隐藏参数)打印 inlining 信息
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

#  禁止某个方法 inlining
-XX:CompileCommand=dontinline,*JIT2.square

# 打印编译信息
-XX:+PrintCompilation

二、反射优化

        先看下下面的代码

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");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

        foo.invoke() 方法前面 0 ~ 15 次调用使用的是 MethodAccessorNativeMethodAccessorImpl 实现。

        当调用到第 16 次(从 0 开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值