二、深入JVM即时编译器JIT,优化Java编译

一.类编译加载执行过程

1、类编译

反编译来看看一个 class 文件结构中主要包含了的信息:
在这里插入图片描述

2、类加载

当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。
不同的实现类由不同的类加载器加载,JDK 中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现加载,而程序中的类文件则由系统加载器(AppClassLoader )实现加载。
在类加载后,class 类文件中的常量池信息以及其它数据会被保存到 JVM 内存的方法区中。

3、类连接

类在加载进来之后,会进行连接、初始化,最后才会被使用。在连接过程中,又包括验证、准备和解析三个部分。

1.验证

验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。

2.准备

为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值仍然为 0。

3.解析

将符号引用转为直接引用的过程。我们知道,在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

4、类初始化

二.即时编译(JIT,可以极大的提高Java执行效率)

初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。
最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

1、即时编译器类型(JDK8)

在 HotSpot 虚拟机中,内置了两个 JIT,分别为 C1 编译器和 C2 编译器,这两个编译器的编译过程是不一样的。
C1 编译器(Client Compiler )是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,例如,GUI 应用对界面启动速度就有一定要求。
C2 编译器(Server Compiler)是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。

JDK8默认的即时编译器是混合类型,即两种类型都在使用,由编译器决定如何编译。
在这里插入图片描述

如果只想开启C2编译器,可ui选择关闭分层编译

-XX:-TieredCompilation

如果只想用 C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1。

-XX:TieredStopAtLevel=1

强制虚拟机只运行解释器不允许运行编辑器

-Xint

强制虚拟机只运行编辑器

-Xcomp

2、热点探测技术(识别哪些代码由编辑器,哪些代码由解释器运行)

在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。
虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

1.方法调用计数器

:用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定;而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。

2.回边计数器

用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,-XX: OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。

三.编译优化技术

1、方法内联

优化前代码

private int add1(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
    return x1 + x2;
}

优化后代码

private int add1(int x1, int x2, int x3, int x4) {
    return x1 + x2+ x3 + x4;
}
1.什么是方法内联
  • 调用一个方法通常要经历压栈和出栈。
  • 方法中调用另一个方法是将程序执行顺序转移到存储该方法的内存地址,将方法的内容执行完后,再返回到执行该方法前的位置。这种执行操作要求在执行前保护现场并记忆执行的地址,执行后要恢复现场,并按原来保存的地址继续执行。 因此,方法调用会产生一定的时间和空间方面的开销。

方法内联就是指的将目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
比如A调用B,将A和B合并,减少一次压栈弹栈,减少方法的记忆。

2.方法内联优化

JVM 会自动识别热点方法,并对它们使用方法内联进行优化。我们可以通过 -XX:CompileThreshold 来设置热点方法的阈值。但要强调一点,热点方法不一定会被 JVM 做内联优化,如果这个方法体太大了,JVM 将不执行内联操作。而方法体的大小阈值,我们也可以通过参数设置来优化:

热点方法,默认情况下,方法体大小小于 325 字节的都会进行内联,可以通过 -XX:MaxFreqInlineSize=N 来设置大小值;

 -XX:MaxFreqInlineSize=N

非热点方法,默认情况下,方法大小小于 35 字节才会进行内联,我们也可以通过 -XX:MaxInlineSize=N 来重置大小值。

-XX:MaxInlineSize=N

热点方法的优化可以有效提高系统性能

  1. 通过设置 JVM 参数来减小热点阈值或增加方法体阈值,以便更多的方法可以进行内联,但这种方法意味着需要占用更多地内存;
  2. 在编程中,避免在一个方法中写大量代码,习惯使用小方法体;
  3. 尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查。

2、逃逸分析

逃逸分析(Escape Analysis)是判断一个对象是否被外部方法引用或外部线程访问的分析技术,编译器会根据逃逸分析的结果对代码进行优化。


-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析

-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭就可以了
1.栈上分配(JDK8尚未实现)
2.锁消除(如果当前资源并没有其他线程去争抢,编译器会消除掉这个锁)

在非线程安全的情况下,尽量不要使用线程安全容器,比如 StringBuffer。由于 StringBuffer 中的 append 方法被 Synchronized 关键字修饰,会使用到锁,从而导致性能下降。
但实际上,在以下代码测试中,StringBuffer 和 StringBuilder 的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除。

3.标量替换(如果创建的对象未被外部访问,可以不用创建这个对象,而用其他的变量代替)

逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换。

优化前

   public void foo() {
        TestInfo info = new TestInfo();
        info.id = 1;
        info.count = 99;
          ...//to do something
    }

如果Info并没有被外部访问,优化后


   
   public void foo() {
        id = 1;
        count = 99;
        ...//to do something
    }
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐