5.深入理解JVM执行引擎

30 阅读5分钟

1.前端编译与后端编译

  • 前端编译:Java文件编译为class文件
  • 后端编译:class字节码编译为机器指令

image.png

2.字节码指令如何执行?

image.png

  • 解释执行:按字翻译为机器码。
  • 编译执行:将要执行的字节码指令提前编译维护到CodeCache中,执行时从缓存中取。
  • 即时编译器JIT:将热点代码提前编译,放进缓存。

java -version 可以看到目前用的什么执行方式。

  • mixed mode 混合模式
  • interpreted mode 解释执行模式
  • compiler mode 编译执行模式 由于内存限制,启动预热时间要求默认使用混合模式

3.热点代码识别

  • 热点探测:判断代码是不是热点,需不需要即时编译。hotspot采用基于计数器的热点探测方法,计数器数值达到阈值触发即时编译。
    • 方法调用计数器 image.png
    • 回边计数器:统计循环体代码执行次数

image.png

4.客户端编译器C1与服务端编译器C2

  • C1:相当于是一个初级翻译。编译过程中,C1会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。启动快,占用内存小。但是翻译出来的机器码优化程度不太高。比较适合于一些小巧的桌面应用,因此也称为客户端编译器。
  • C2:相当于是一个高级翻译。编译过程中,C2会对字节码进行更激进的优化,优化后的佮代码执行效率更高。但是相应的,工作量也变得更大了。C2的启动更慢,占用内存也更多。进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。启动慢,占用内存多,执行效率高。比较适合于一些资源充裕的服务级应用,因此也称为服务端编译器。

4.1 分层编译

由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花费的时间便会越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行阶段的速度也有所影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译的功能,分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次。

等级描述性能
0程序纯解释执行,并且解释器不开启性能监控功能(Profiling)1
1使用C1编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化。不开启性能监控功能。4
2仍然使用C1编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。3
3仍然使用C1编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。2
4使用C2编译器将字节码编译为本地代码,相比起C1编译器,C2编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。5
public class JitDemo {

    private int add(int x){
        return x+1;
    }

    public static void main(String[] args) {
        int a = 0;
        JitDemo demo = new JitDemo();
        long l = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            a = demo.add(a);
        }
        System.ou
        .println("a= "+a);
        System.out.println(">>>>>>>>"+(System.currentTimeMillis()-l));
    }
}
  • JDK8 中提供了参数 -XX:TieredStopAtLevel=1 可以指定使用哪一层编译模型。

  • 使用-Xint , -Xcomp, -XX:TieredStopAtLevel=1,-XX:TieredStopAtLevel=5比较

5.后端编译优化技术

wiki.openjdk.java.net/display/Hot…

5.1 方法内联

image.png

  • 方法内联的前提是要足够的循环次数,成为热点代码
  • -XX:+Inline 启用方法内联。默认开启。
  • -XX:InlineSmallCode=size 用来判断是否需要对方法进行内联优化。如果一个方法编译后的字节码大小大于这个值,就无法进行内联。默认值是1000bytes。
  • -XX:MaxInlineSize=size 设定内联方法的最大字节数。如果一个方法编译后的字节码大于这个值,则无法进行内联。默认值是35byt
  • -XX:FreqInlineSize=size 设定热点方法进行内联的最大字节数。如果一个热点方法编译后的字节码大于这个值,则无法进行内联。默认值是325bytes。
  • -XX:MaxTrivialSize=size 设定要进行内联的琐碎方法的最大字节数(Trivial Method:通常指那些只包含一两行语句,并且逻辑非常简单的方法。比如像这样的方法

提高方法内联的概率:

  • 在编程中,尽量多写小方法,避免写大方法。方法太大不光会导致方法无法内联,另外,成为热点方法后,还会占用更多的CodeCache。
  • 在内存不紧张的情况下,可以通过调整JVM参数,减少热点阈值或增加方法体阈值,让更多的方法可以进行内联。
  • 尽量使用final, private,static关键字修饰方法。方法如果需要继承(也就是需要使用invokevirtual指令调用),那么具体调用的方法,就只能在运行这一行代码时才能确定,编译器很难在编译时得出绝对正确的结论,也就加大了编译执行的难度。

5.2 逃逸分析

  • 对象在方法体中被定义之后,有没有被外部方法引用。
    • 例如:作为参数传递到外部方法中或者被其他线程访问。
  • 从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
  • 如果能证明对象不会逃逸,那么这种的就可以进一步优化。
5.2.1 标量替换

把对象拆解为基本的数据类型,用来替代原来的对象。

5.2.2 栈上分配

标量替换完成后,分配在栈帧之类,方法出栈后自动销毁,不用经过gc。

image.png

6.锁消除

消除jvm确认无用的不生效的synchronized关键字