写在文章开头
在现代软件开发中,Java 语言因其跨平台性和强大的生态系统而广受欢迎。然而,性能一直是开发者关注的重点之一。为了提升 Java 应用的运行效率,Java 虚拟机(JVM)引入了多种优化技术,其中最引人注目的莫过于即时编译(Just-In-Time Compilation,简称 JIT)。本文将深入探讨 JVM 中的 JIT 编译技术,揭示其背后的原理和工作机制,并介绍如何通过配置和调优来最大化应用性能。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
详解JIT编译技术
即时编译的执行点
在初始化阶段完成后,执行引擎不断将调用到的字节码翻译成机器码交由计算机执行,Java
字节码转为机器码之间还有一步转换,我们称之为既时编译
:
最初Java
字节码文件是直接通过解释器( Interpreter )
解释为机器码直接运行的。对于某些执行频率比较频繁的代码,我们可以称之为热点代码
,JIT
就会针对这些热点代码进行相应的优化并缓存,以提升程序的运行效率:
即时编译器类型有哪些?
我们以HotSpot
虚拟机为例,该虚拟机内置了两个JIT
编译器,分别为:
C1编译器:
主要关注点在于局部性优化,常用于那些执行时间短,或者要求快速启动的应用程序,例如GUI
应用程序。C2编译器:
常用于长期运行且对峰值性能有高要求的服务器。
所以我们也称C1编译器和C2编译器为 Client Compiler
或者Server Compiler
。
在 Java7
之前,需要根据程序的特性来选择对应的 JIT
,虚拟机默认采用解释器和其中一个编译器配合工作。
Java7
引入了分层编译,这种方式综合了 C1
的启动性能优势和 C2
的峰值性能优势,我们也可以通过参数 “-client”“-server”
强制指定虚拟机的即时编译模式。分层编译将 JVM
的执行状态分为了 5 个层次:
第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译; 第 1 层:可称为 C1
编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling; 第 2 层:也称为 C1 编译,开启
Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译; 第 3 层:也称为 C1 编译,执行所有带
Profiling 的 C1 编译; 第 4 层:可称为 C2
编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
在 Java8
中,默认开启分层编译,-client
和 -server
的设置已经是无效的了。如果只想开启
C2
,可以关闭分层编译(-XX:-TieredCompilation)
,如果只想用
C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1
。
我们可以使用java -version
查看当前编译的编译模式,可以看到笔者服务器的JVM
使用的就是混合编译模式:
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
当然,如果我们想将编译器模式改为解释器模式,就可以键入下面这条命令:
java -Xint -version
如果我们想强制运行JIT
编译模式,也可以使用
java -Xcomp -version
JIT的热点探测
什么是JIT热点探测
HotSpot
虚拟机判定热点代码是基于两种计数器进行的,分别是方法调用计数器(Invocation Counter)
和回边计数器(Back Edge Counter)
,只有执行代码符合他们的标准且达到他的设置的阈值时才会进行JIT
编译优化。
方法调用计数器
方法调用器会针对方法的执行频率进行相应的优化,当某个方法执行次数超过阈值时,就会触发JIT
编译优化,这个阈值我们可以通过jinfo
查看:
jinfo -flag CompileThreshold pid
以笔者某个java
进程为例,可以看到JVM
设置的方法调用计数器判定是否是热点代码的条件为调用次数达到10000
次:
-XX:CompileThreshold=10000
这也就意味着当方法调用在一段时间(而非永久叠加)次数达到10000
次的时候,就会提交一个编译请求,后续执行时都直接用缓存中的编译后的机器码直接运行:
回边计数器
在字节码遇到控制流后跳转的操作我们称之为回边,回边计数器判定代码为热点代码的条件是一个代码在循环体内达到回边计数器要求的阈值,而这个阈值我们也可以通过jinfo
查看
jinfo -flag OnStackReplacePercentage pid
以笔者的进程为例可以看到当回边次数达到140
时也会执行相应的JIT
优化,即当这段代码被判定为热点代码时,JVM
就会进行一种栈上编译的优化操作,它会将这段代码编译为最优逻辑保存到本地内存,在执行循环体的期间,直接使用缓存中的机器码:
-XX:OnStackReplacePercentage=140
注意:与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
JIT编译优化技术
方法内联
我们都知道方法调用会经历一个压栈和出栈的操作,执行调用方法时会将地址转移到存储该方法的起始地址上,待调用结束后,在返回原来的位置。
这就意味着一个方法调用另一个方法时,就需要保存当前方法执行位置,栈上压入被调用方法,执行完成后,恢复现场继续执行之前执行的方法。因此方法调用期间是有一定的时间和空间的开销的。
所以JIT
会对那些方法调用方法非常频繁的代码执行方法内联:
private int