Java jit,jit编译原理
一.口译员
Java程序运行时,主要执行字节码指令。一般这些指令会被依次解释和执行,也就是解释和执行。
但是那些被频繁调用的代码,比如那些被频繁调用或者在for循环中的代码,如果按照解释执行,效率是非常低的。
上述代码称为热点代码。因此,为了提高热代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并在各个层面进行优化。
2.编译器完成这项任务的编译器称为实时编译器,简称JIT编译器。
集成的JVM编译器有两种模式:
客户端编译器
服务器编译器
客户端编译器注重启动速度和局部优化。HotSpot VM使用客户端编译器,简称C1编译器。服务器编译器注重全局优化,运行时性能更好。因为更多的全局分析,启动速度会变慢。Hotspot VM有两种:C2编译器(默认)和Graal编译器(暂时不讨论)。1.传统编译器在JDK1.8的HotSpot虚拟机中内置了两个JIT,分别是C1编译器和C2编译器。
1)C1编译器C1编译器是一个简单快速的编译器,它的主要侧重点是局部优化,适用于执行时间短或者对启动性能有要求的程序。比如GUI应用对界面启动速度有一定要求,C1也叫客户端编译器。
C1编译器几乎没有优化代码。
2)C2编译器C2编译器是一款优化长时间运行的服务器端应用程序性能的编译器,适用于执行时间较长或要求峰值性能的程序。根据它们的适应性,这种即时编译也被称为服务器编译器。
但是C2电码太复杂了,没人能维护它!这就是为什么用Java写的Graal编译器被开发出来取代C2(JDK10起步)。
3)分层编译C1和C2各有利弊。为了整合它们的优势,实现编译速度和执行效率的平衡,JVM引入了一种策略:分层编译。
级别0(解释执行):采用解释执行,不启用分析。
一级(简单C1编译):采用C1编译器进行简单可靠的优化,不开启性能监控功能。
二级(有限C1编译):采用C1编译器进行更优化的编译,开启性能监控功能,统计方法计数器和后沿计数器的值。
第三级(完全C1编译):充分使用C1编译器的所有功能,充分开启性能监控功能。
四级(C2编译):用C2编译器完成编译,并完成优化。
注意:分层编译只能在服务器编译器模式下启用。
其中,这些级别的执行效率如下:4 1 2 3 0。
C2代码的执行效率比C1高30%以上。
1 3主要原因是性能监控功能开启越多,性能成本越高。
列出几个常用的编译路径,如下图所示:
在Java7之前,需要根据程序的特点选择相应的JIT。默认情况下,虚拟机使用解释器与其中一个编译器一起工作。
Java7和后来引入了分层编译,它结合了C1的启动性能优势和C2的峰值性能优势。当然,我们也可以通过参数强制虚拟机的即时编译模式。
在Java8中,分层编译是默认打开的。
1.可以通过java -version命令行直接查看当前系统使用的编译模式(默认分层编译)。
2.使用-Xint参数强制虚拟机在仅编译器模式下运行。
3.使用-Xcomp强制虚拟机以仅JIT编译模式运行。
2.GraalVMGraalVM是一个高性能的JDK发行版,旨在加速用Java和其他JVM语言编写的应用程序的执行,支持JavaScript、Ruby、Python等多种流行语言。
Graal编译器是由GraalVM和HotSpotVM(来自JDK10)共同拥有的服务器端即时编译器,是C2编译器的替代品。
格拉尔和C2的区别。Graal和C2最明显的一个区别就是,Graal是用Java写的,而C2是用c写的,相对来说,Graal更模块化,更容易开发和维护。毕竟,即使是C2的开发商也不想维护C2。
很多人认为用C写的C2肯定比Graal快。事实上,在充分预热的情况下,Java程序中的热代码早已通过即时编译转化为二进制代码,执行速度不亚于静态编译的C程序。
Graal的内联算法对新的语法和语言更加友好,比如Java 8的lambda表达式和Scala语言。
2)JVMCI前面解释过,编译器是Java虚拟机中相对独立的模块,主要负责接收Java字节码,生成可以直接运行的二进制代码。
传统上(JDK8),即时编译器是与Java虚拟机紧密耦合的。也就是说,对实时编译器的更改需要重新编译整个Java虚拟机。这对于开发相对活跃的Graal来说显然是不可接受的。
为了实现Java虚拟机与Graal的解耦,我们引入了Java虚拟机编译器接口(JVMCI ),它将实时编译器的功能抽象为一个Java级别的接口。这样我们就可以在Graal所依赖的JVMCI版本不变的情况下,只需要更换Graal编译器相关的jar包(Java 9以后的jmod文件)就可以升级Graal。
3)aot提前编译(AOT,提前编译)。在编译时,他会把所有相关的东西,包括一个基础VM,编译成机器码(二进制)。
Graal的aot是“GraalVM”中的一项技术,它的优点是可以更快地启动一个java应用程序(以前如果要启动一个java程序,需要先启动jvm然后加载java代码,然后立即编译。将字节码类进机器码,非常耗时耗内存,但如果使用AOT,可以得到更小更快的镜像,适合云部署)。
4)特点GraalVM是一个高性能的嵌入式多语言虚拟机,可以运行不同的编程语言。
基于JVM的语言,如Java、Scala、Kotlin和Groovy解释语言,如JavaScript、Ruby、R和Python,以及与LLVM一起工作的本机语言,如C、C、Rust和SwiftGraalVM,都是为了在不同的环境中运行程序而设计的。
在JVM中编译成独立的本地镜像(没有JDK环境),将Java和本地代码模块集成到一个更大的应用中。三、热代码热代码是指那些被频繁调用的代码,比如那些被频繁调用或者在for循环中的代码。这些重新编译的机器码会被缓存起来供下次使用,但是对于那些很少执行的代码来说,这个编译动作就是浪费。
JVM提供了一个参数"-xx: reservedDecacheSize "来限制CodeCache的大小。也就是说,JIT编译的代码会放在CodeCache中,默认大小为240M。
如果这个空间不够,JIT将无法继续编译,编译执行将变成解释执行,性能将降低一个数量级。同时,JIT编译器会一直尝试优化代码,导致CPU使用率增加。
通过Java查询-XX:print flag final版本:
1)热点检测热点虚拟机中的热点检测是JIT优化的条件。热点检测基于计数器。使用该方法的虚拟机为每个方法设置计数器,以计算该方法的执行次数。如果执行时间超过某个阈值,则被认为是“热点方法”
虚拟机为每个方法准备了两种计数器:方法调用计数器和后沿计数器。在确定虚拟机运行参数的前提下,这两个计数器有一定的阈值。当计数器超过阈值并溢出时,将触发JIT编译。
2)方法调用计数器用于计算方法被调用的次数。方法调用计数器的默认阈值在客户端模式下是1500次,在服务器模式下是10000次(我们都用服务器,java版查询),可以用-XX: CompileThreshold设置。
通过Java查询-XX:print flag final版本:
四。编译器优化技术JIT编译使用一些经典的编译优化技术对代码进行优化,即通过一些例行检查和优化,可以智能编译出运行时性能最好的代码。
1.方法内联。方法内联的优化行为是将目标方法的代码复制到调用方法中,以避免真正的方法调用。
例如,以下方法:
最终将优化为:
JVM将自动识别热点方法,并优化它们对方法内联的使用。
我们可以通过-XX:CompileThreshold来设置hotspot方法的阈值。
但是,应该强调的是,hotspot方法可能不会被JVM内联优化。如果方法体太大,JVM将不会执行内联操作。
和方法体的大小阈值,我们也可以通过设置参数来优化它:
经常执行的方法,默认情况下,所有体大小小于325字节的方法都将被内联。我们可以通过-xx:frequency linesize=n来设置大小值;
这不是一个经常执行的方法。默认情况下,在内联之前,方法大小小于35字节。我们也可以通过-xx: maxinlinesize=n来重置大小值。
也就是说,当方法大小满足:frequency linesize(默认325字节)和方法大小FreqInlineSize(默认35字节)时,将触发方法内联优化。
热点优化可以有效提高系统性能。通常,我们可以通过以下方式改进方法内联:
通过设置JVM参数来降低热点阈值或者提高方法体阈值,这样可以内联更多的方法,但是这种方法意味着需要占用更多的内存;编程中,避免一个方法写很多代码,习惯使用小方法体;尽可能使用final、private和static关键字来修饰方法。由于继承,编码方法将需要额外的类型检查。2.锁消除在非线程安全的情况下,尽量不要使用线程安全容器,比如StringBuffer。因为StringBuffer中的append方法被Synchronized关键字修改,所以将使用锁,从而导致性能下降。
但实际上,在下面的代码测试中,StringBuffer和StringBuilder的性能基本相同。这是因为在本地方法中创建的对象只能被当前线程访问,而不能被其他线程访问。读写这个变量肯定不会有竞争。此时JIT编译会锁定这个对象的方法锁。
在下面的测试代码中,StringBuffer和StringBuilder的性能基本没有区别。这是因为在本地方法中创建的对象只能被当前线程访问,而不能被其他线程访问。读写这个变量肯定不会有竞争。此时JIT编译会锁定这个对象的方法锁。
移除锁并关闭它-测试发现性能差异有点大。
-XX: EliminateLocks打开锁消除(jdk1.8默认打开,其他版本不测试)
-XX:-EliminateLocks关闭锁并消除它们。
3.标量替换转义分析证明一个对象不会被外部访问。如果这个对象可以拆分,在程序实际执行的时候可能不会创建,而是直接创建它的成员变量。对象拆分后,对象的成员变量可以在堆栈或寄存器上分配,原对象不需要分配内存空间。这种编译优化称为标量替换(前提是需要打开转义分析)。
-XX: DoEscapeAnalysis:开启转义分析(jdk1.8默认开启)
-XX:-DoEscapeAnalysis:关闭转义分析。
-xx: elite allocations:标量替换处于打开状态(默认情况下jdk1.8处于打开状态)
-XX: -XX:-EliminateAllocations:关闭标量替换4。逸出分析逸出分析的原理:分析一个对象的动态范围。当一个对象在一个方法中定义时,它可能被一个外部方法引用。
例如,将call参数传递给其他方法,这称为方法转义。甚至可能被外部线程访问,比如给其他线程访问的变量赋值,这叫线程转义。
从转义到方法转义再到线程转义,称为对象从低到高的不同转义程度。
如果确定某个对象不会从线程中逃逸,那么让该对象在堆栈上分配内存可以提高JVM的效率。
当然,转义分析技术属于JIT优化技术,所以JIT只有符合热代码才会被优化。此外,如果要将对象分配给堆栈,则需要将其拆分。这种编译优化称为标量替换技术。
如果escape分析的对象可以在栈上分配,那么对象的生命周期就跟着线程走,垃圾回收就没必要了。如果经常调用这个方法,性能会大大提高。
使用转义分析后,满足转义的对象被分配到堆栈上。
如果不开启转义分析,所有对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行缓慢。
最后,可以总结如下:
1.当要创建一个对象时,将判断该对象是否是热代码。2.如果不是直接在堆中创建的(不考虑对象超大,后续堆中的动态年龄判断和大小判断等。).3.如果是热代码(服务器执行10000次以上),会判断是否启用转义分析,是否可以转义。4.如果没有启用转义分析或者转义分析的条件不满足,那么除了第一次之外,与第二步的执行流程5相同,然后继续确定是否开始标量替换。6.如果标量替换没有开始,它与第二步的执行流程7相同。如果标量替换开始了,对象就在堆中分配。
参考:
https://blog.csdn.net/qq_44377709/article/details/125075391
https://zhuanlan.zhihu.com/p/439372218
版权归作者所有:原创作品来自博主肖波,转载请联系作者取得授权,否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。