vlambda博客
学习文章列表

JVM调优之HotSpot编译优化

.class字节码有两种执行方式

  • 解释执行:由解释器一行一行翻译执行

  • 编译执行:把字节码编译成机器码,直接执行机器码

    解释执行的优势在于没有编译的等待时间,但是性能相对较差;编译执行运行效率会高很多,但是带来了额外的开销。

查询自己的java是哪种执行的命令

java -version

代表混合模式

// 解释执行模式java -Xint -version// 编译执行模式java -Xcomp -version// 混合模式执行,默认情况java -Xmixed -version

    一般情况下,都由解释器解释执行

    但是如果当虚拟机发现某个方法或者代码块运行特别频繁的时候,就会认为这些代码是“热点代码”。为了提高热点代码的执行效率,会用即时编译器(也就是JIT)把这些热点代码编译成与本地平台相关的机器码,并进行个层次的优化。

Hotspot的即时编译器-C1

  •     是一个简单快速的编译器

  •     主要关注局部性变化

  •     适用于执行时间较短或对启动性能有要求的程序。例如:GUI应用对界面启动速度就有一定要求。也被称为是Client Compiler

Hotspot的即时编译器-C2

  •     是为长期运行的服务端应用程序做性能调优的编译器

  •     适用于执行时间较长或对峰值性能有要求的程序

  •     也被称作是Server Compiler

从jdk7开始,引入了分层编译的概念,细分为五种编译级别

  • 0: 解释执行

  • 1: 简单C1编译:会用C1编译器进行一些简单的优化,不开启监控性能(Profiling)

  • 2: 受限的C1编译器:仅仅执行带方法调用次数以及循环回边执行次数 Profiling 的C1 编译

  • 3: 完全C1编译:会执行带有所有Profiling的C1代码

  • 4: C2编译(最高级别):使用C2编译器进行优化,该级别会启用一些编译耗时较长的优化,一些情况下会根据性能监控信息进行一些非常激进的性能优化

       级别越高,应用启动越慢,优化的开销越高,峰值性能也越高。

分层编译-JVM参数配置示例:

// 只想开启C2,禁用中间编译层(123层)-XX:-TieredCompilation//只想开启C1,参数为几就是开启第几层-XX:-TieredCompilation -XX:TieredStopAtLevel=1

热点代码的探测:

    基于采样的热点探测:相当于有一个栈来维护调用情况,当有线程调用某个方法时就入栈,则每次检查在栈中的就是调用比较多的方法

    基于计数器的热点探测:Hotspot就是使用这种方法,它为每个方法准备两类计数器:

        方法调用计数器:用于统计方法被调用的次数,在不开启分层编译的情况下,在C1编译器下的默认阈值是1500次,在C2模式下是10000次。也可以用-XX:CompileThreshold=X指定阈值。

        回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边(Back Edge)。在不开启分层编译的情况下,C1编译器下面的默认阈值13995, C2默认为10700,可使用-XX:OnStackReplacePercentage=X指定阈值。建立回边计数器的目的主要是为了触发OSR(On StackReplacement)编译。

 https://www.zhihu.com/question/45910849/answer/100636125

     当开启分层编译时,JVM会根据当前待编译的方法数以及编译线程数来动态调整阈值,-XX:CompileThreshold、-XX:OnStackReplacePercentage 都会失效。

方法调用计数器的流程

如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的频率,即一段时间之内被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器,那么这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期时间,单位是秒。

回编计数器的流程


方法内联的条件:

    方法体足够小,如果热点方法的方法体小于325字节会尝试内联,可用-XX:FreInlineSize修改大小;如果是非热点方法,方法体小于35字节,也会尝试内联,可用-XX:MaxInlineSize修改大小

    被调用方法运行时的实现可以被唯一确认,比如static、private、final方法,JIT可以唯一确认具体实现代码;对于public的实例方法,指向的可能是自身、父类、子类的代码,当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联。

逃逸分析

    分析动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

    如果能证明一个对象不会逃逸到方法或者线程之外,或者逃逸程度比较低(只逃出方法而不会逃出线程),则可以考虑为这个对象采取不同程度的优化。

  • 栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上面分配内存是很不错的,对象所占用的空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部变量和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法结束而自动销毁了,垃圾收集子系统的压力会下降很多。栈上分配支持方法逃逸,但不支持线程逃逸。

  • 标量替换:如果一个对象可以继续分解,那就称为聚合量;如果不能在进行分解像int、long等类型的变量,就被称为标量。把一个程序的Java对象拆散,根据程序的访问情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。

           假如逃逸分析证明一个对象不会被外部访问,并且这个对象可拆散,那么程序真正执行时就不会去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视为栈上分配的一种特例,实现更简单,但是逃逸程度要求更高,不允许对象逃逸出方法范围内。

  • 同步消除:线程同步本身是一个相对耗时的过程,如逃逸分析判断一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量就不会有读写竞争,则对这个变量的同步措施就可以消除掉。

以上用到的大部分命令:

// 以上使用的参数-Xmixed // 混合模式运行-Xint // 设置JVM的执行模式为解释执行模式-Xcomp // JVM优先以编译模式运行,不能编译,以解释模式运行。-XX:-TieredCompilation // 禁用中间层编译-XX:TieredStopAtLevel // 到哪个分层停止-XX:CompileThreshold=X // 指定方法调用计数器阈值(关闭分层编译才有效)-XX:OnStackReplacePercentage=X // 指定回编计数器阈值(关闭分层才有效)-XX:-UseCounterDecay // 关闭方法调用计数器热度衰减-XX:CounterHalfLifeTime // 指定方法调用计数器半衰周期(秒)-XX:FreInlineSize  // 修改热点方法内联参数大小-XX:MaxInlineSize // 修改非热点方法内联参数大小

OSR:是一种运行时替换栈帧的技术,可看下面这篇文章,解释得很仔细~

https://www.zhihu.com/question/45910849/answer/100636125