JIT耗时优化

优质博文:IT-BLOG-CN

一、背景

业务流量突增,机器直接接入大量流量QPS2000JITGC会消耗太多CPU资源,导致1-2分钟时间内的请求超时导致异常,因此采用流量预热的方式,让机器逐步接入流量,需要预热时长3min。目前服务接入HPA,通过HPA自动扩缩容应用流量变化,当流量激增时,对机器的启动速度带来了挑战 ,之前通过Swift优化点火时间,已经将机器从容器创建到可接入流量优化到2分钟左右,但3min的预热时长成为了应对流量激增的瓶颈,因此优化机器从接入流量到能稳定服务的时长,目标缩减到2min以内。

什么是服务预热: Java应用在刚启动的时候处理相应速度会很慢,只有当热点代码执行了一定次数以后,相应速度才会达到一个稳定状态。由于Java慢启动现象的存在,多数情况下我们有必要对Java应用进行预热,以防止客户端在调用过程中,因为服务器重启或发布事件,而出现大量慢请求。

流量接入后younggc耗时:峰值900ms左右,最大次数18次

二、优化思路

名词解释

JIT(Just In Time) 即时编译器: java程序是解释执行的,即运行时将字节码解释为机器码来执行,因此性能差;为了优化Java性能,jvm引入的编译器,随着程序的执行,编译器会将热点代码编译优化为本地代码,来获取更高的执行效率

jvm中集成了两种编译器:

【1】Client Compiler:如C1编译器,注重启动速度和局部的优化,C1的启动速度开,但是峰值性能比C2要差;

【2】Server Compiler:如C2编译器、Graal编译器,关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢;

【3】分层编译:为了综合Client ComplierServer Compiler的特性,在启动速度和峰值性能之间取得平衡,java7开始引入分层编译,分为5层:

■ 解释执行。

■ 执行不带profilingC1代码。

■ 执行仅带方法调用次数以及循环回边执行次数profilingC1代码。

■ 执行带所有profilingC1代码。

■ 执行C2代码。

方法内联: 编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段,JIT大部分的优化都是在内联的基础上进行的;

逃逸分析: 编译器,根据新建对象是否被存入堆中以及是否传入未知代码(未内联代码)中,判断对象是否逃逸,对未逃逸对象进行锁消除、栈上分配优化;

更多内容参考:JIT & AOP

优化思路

【1】通过调整JVM参数,提高JIT效率;

● 增加JIT线程;

● 调整内联参数,减少内联失败;

● 关闭分层编辑,直接进行C2编译;

● 关闭逃逸分析,让出资源做其他优化;

【2】更换更新的Server Compiler:Graal编译器使用Java编写,对于Java而言,尤其是新特性,比如Lambda/Stream等更优化。

【3】使用AOT:提前编译,在运行时将Java方法动态编译为本地AOT代码,并将它们存储在共享类缓存中,以此提升启动速度,如:DragonWall/openJ9

【4】业务代码层优化:减少代码量,针对目前基础策略灰度体检,代码体谅大,灰度结束后,代码量减少,JIT应当有所好转。

三、优化过程

优化前机器参数:JIT耗时1.7minGC峰值600ms

调整 JVM参数

【1】采用GraalVM编译器: 有效果,但效果没有关闭分层编译好。

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler

【2】增加JIT线程数: 默认15个线程

java 复制代码
-XX:+CICompilerCountPerCPU=false
-XX:CICompilerCount=16

【3】增加内联机器码大小阈值,减少内联失败。同时,增加内联调用次数阈值,延迟内联: 无效果,短暂延迟了JIT耗时峰值;

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000

【4】关闭分层编译: 镜像效果明显

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:-TieredCompilation

【5】关闭逃逸分析: 效果不明显,有持续耗时高峰,不可用

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:-DoEscapeAnalysis

AOT

【1】通过openj9AOT替换JIT 启动性能要好一些,但是稳定后吞吐量和延迟都要差一点,然后启动时会有部分超过100ms(大概是首分钟的95线)
【2】使用DragonWall11 不支持JWarmup不可用。

JWarmup:让JVM提前知道哪些方法热的,在处理请求之前就让这些方法提前被编译掉,从而避免了前面边解释,边编译的开销。

代码优化

减少代码量: JIT耗时明显下降。

JVM参数符合使用

【1】采用GraalVM & 关闭分层编译: JIT峰值没有改善,且点火时异常增高。最终项目启动成功的成本100288ms不可用

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
-XX:-TieredCompilation

【2】采用GraalVM & 增加内联机器码大小阈值,减少内联失败 & 增加内联调用次数阈值,延迟内联 JTI峰值没有改善,且点火时长异常高。不可用

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
-XX:+UnlockDiagnosticVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000

【3】关闭分层编译 & 增加内联机器码大小阈值,减少内联失败 & 增加内联调用次数阈值,延迟内联 JTI峰值没有改善,且GC耗时过高。不可用

java 复制代码
-XX:+UnlockExperimentalVMOptions
-XX:-TieredCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000

四、优化结果

【1】JIT-MAXJIT点火耗时Max,从1.9min左右,2月1日关闭分层编译后减少到1.6min左右,代码优化后降到55s左右

【2】JIT-AVGJIT平均耗时,从原来的10S,2月1日关闭分层编译后减少到7.5s左右,代码优化后降到5s左右

五、结论

【1】分层编译对JIT耗时有增益效果,但是由于机器差异,对最大耗时的优化不是很明显,从平均耗时看差异较大;

【2】代码重构后,代码量减少,对最大JIT编译耗时优化效果比较明显,平均耗时也有所下降;

【3】优化QPM数据采集准确性,减少由于数据采集延迟带来频繁扩缩容,减少JIT高峰数量;

相关推荐
胚芽鞘68111 分钟前
查询依赖冲突工具maven Helper
java·数据库·maven
Charlie__ZS16 分钟前
若依框架去掉Redis
java·redis·mybatis
roc_lab38 分钟前
Spring Cloud Feign默认不支持重定向解决方案
spring cloud
咖啡啡不加糖1 小时前
RabbitMQ 消息队列:从入门到Spring Boot实战
java·spring boot·rabbitmq
玩代码1 小时前
Java线程池原理概述
java·开发语言·线程池
NE_STOP1 小时前
SpringBoot--如何给项目添加配置属性及读取属性
java
水果里面有苹果1 小时前
20-C#构造函数--虚方法
java·前端·c#
%d%d21 小时前
python 在运行时没有加载修改后的版本
java·服务器·python
金銀銅鐵1 小时前
[Kotlin] 单例对象是如何实现的?
java·kotlin
泰勒疯狂展开1 小时前
Java研学-MongoDB(三)
java·开发语言·mongodb