一个隐藏比较深的问题。项目使用grpc完成远程调用,但是grpc定义的pd文件,由于message中参数过多,达到190多个,在pb的该message某次新增一个字段后。项目的CPU使用率直接由10%飙升到70%。
1. 基础知识
1.1 什么叫做JIT
在计算机技术中,即时编译(just-in-time,缩写JIT;又译及时编译、实时编译),也被称为动态编译或者运行时编译,是一种执行计算机代码的方法,这种方法涉及在程序执行过程中(在执行期)而不是执行之前进行编译。
1.2 JIT工作原理
在字节码编译的系统中,源代码被转换为称为字节码的中间表示形式。字节码不是任何特定计算机的机器代码,可以在计算机体系结构之间移植(即JAVA的跨平台性)。然后可以在虚拟机上解释或者运行字节码、JIT编译器在许多(或全部、很少)部分读取字节码,并将他们动态的编译成机器代码,以便程序更快速运行。这可以针对每个文件、每个函数甚至任何任意代码片段进行编译;代码可以在即将执行时进行编译(因此称为“即时”),然后缓存并在以后重用,无需重新编译。
缺省情况下,启用JIT编译器。在编译方法时,JVM直接调用该方法的已编译代码,而不是对代码进行解释。理论上,如果编译不需要占用处理器时间和内存,那么编译每个方法都可能使JAVA程序速度接近于本机应用程序的速度。
JVM流程.png图片来源:如何通俗易懂地介绍「即时编译」(JIT)
疑问?为什么JVM里既有compiler,也有interpreter(解释器)?
JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。
1.3 JIT编译是在项目启动时编译的吗?
不是!JIT编译不需要占用处理器时间和内存。在JVM首次启动时,将调用数千种方法。即使程序最终实现了较高的峰值性能,编译所有这些方法也会对启动时间产生显著影响。实际上,第一次调用方法时不会对方法进行编译。对于每个方法,JVM都会保留一个调用计数,以预定义的编译阈值开始,并在每次调用方法时递减。在调用计数达到零时,将触发方法的即时编译。因此,在JVM启动后将立即编译常用方法,而较长时间(或者根本不编译)不常用的方法。JIT编译阈值帮助JVM快速启动并且还可以提高性能。选择阈值以在启动时与长期性能之间实现最佳平衡。
image.png为了提升执行速度,Hotspot JVM采用了JIT compile技术,JIT编译器将运行频率很高的字节码(热点代码)直接编译为机器码执行以提高性能。
1.4 JIT编译与静态编译
常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言。
为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。
即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,JIT(即时编译器)可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。
2. JIT编译与CPU使用率飙升
上面说到JIT编译,那么他和CPU使用率飙升有什么关系呢?
上面说到,热点代码会被直接编译成机器码执行来提高性能,但是考虑这样一个场景:热点代码每次调用时均逐条解释(Interpreter),是不是需要消耗CPU资源?
那么您又得好奇了,JVM不是采用了JIT技术了吗,为啥热点代码不会直接编译成机器码执行?
2.1 大方法默认关闭JIT编译
上面说到,JVM是默认开启JIT编译器,但是存在一个参数,若一个方法中字节码大小超过8000字节,那么就不允许被JIT编译,而8000这个阈值在产品版的HotSpot里无法被调整。
关键词:
-XX:+DontCompileHugeMethods
-XX:HugeMethodLimit=8000
解决方案:可以通过JVM参数:-XX:-DontCompileHugeMethods
来允许大方法被JIT编译。
但是这种方式存在风险,会导致所有的方法都有可能编译成字节码,但是一旦CodeCache满了,后续的方法都无法编译成字节码,这种方法是存在一定风险的。
2.2 查看方法占用的字节
可以下载:jclasslib Bytecode viewer插件。
使用方法:IDEA字节码学习查看神器jclasslib bytecode viewer介绍
2.3 触发场景以及解决方案
测试案例——Java中一个方法字节码的长度为什么会影响程序并发下的性能?
在实际工作中,一个方法很少能超过8000字节,但是自动生成的方法便存在这种风险。例如本次事故,就是grpc的pb文件中,一个message{}中的属性字段太多导致的。
针对grpc的这种场景:可以增加一个嵌套的message字段,新增的字段都放在这个嵌套结构下。可以解决这种业务场景。
参考资料
IDEA字节码学习查看神器jclasslib bytecode viewer介绍
网友评论