介绍一下JIT优化技术?
想要把高级语言转变成计算机认识的机器语言有两种方式,分别是编译和解释,虽然Java转成机器语言的过程中有一个步骤是要编译成字节码,但是,这里的字节码并不能在机器上直接执行。
JVM中内置了 解释器(interpreter),在运行时对字节码进行解释翻译成机器码,然后再执行。
解释器 的执行方式是 一边翻译、一边执行,因此执行效率较低。为了解决这个低效率问题,HotSppot 引入了JIT 技术(Just-In-Time,即时编译)。此时:
- JVM仍然是通过解释器进行解释执行;
- 但是当JVM发现某个方法或代码块运行时执行的很频繁,就会认为是"热点代码(Hot Spot Code)"。然后JIT 会把部分 热点代码 翻译为 机器码 并进行优化,同时会把翻译后的机器码缓存起来,以备下次使用。
介绍一下 JIT 的两种编译器?
HotSppot 虚拟机中内置了两个 JIT编译器:Client Compiler 和 Server Compiler。
- 客户端编译器(Client Compiler):也被称为 C1 编译器。C1 编译器会对代码进行简单的优化,并专注于提高编译速度。
- 服务端编译器(Server Compiler):也被称为 C2 编译器。它是为了长时间运行的服务器端应用程序而设计的,因此在启动时可能会比 C1 编译器慢。C2 编译器会进行更复杂的优化,如全局优化、内联等,以获得更高的运行效率。
HotSpot 虚拟机提供了哪三种运行模式?
HotSpot 虚拟机提供了三种运行模式,这些模式主要影响即时编译器(JIT)的行为与性能:
-
解释模式(Interpreted Mode) :即所有代码都解释执行,而不经过 JIT 编译。使用
-Xint
参数可以打开这个模式。优点:启动快速,没有编译延迟;缺点:运行效率相对较低;
-
编译模式(Compiled Mode):Java 字节码会由 JIT 编译器编译成本地机器码后执行。这通常包括 C1 和 C2 编译器,它们分别负责客户端编译和服务端编译。
优点:比解释模式 运行效率高;缺点:启动时间较长,因为需要时间来编译代码。
-
混合模式(Mixed Mode):HotSpot 虚拟机的默认模式。在混合模式下,Java 虚拟机会同时使用解释器和编译器。刚开始运行时,字节码由解释器执行,随着程序的运行,热点代码(即执行频率较高的代码)会被 JIT 编译器编译成本地机器码。
优点:结合了++解释模式的快速启动++ 和++编译模式的高效运行++。
-Xint:强制虚拟机运行于解释模式。
-Xcomp:强制虚拟机运行于编译模式。
不指定或使用 -Xmixed:默认使用混合模式。
可以通过java -version
命令查看运行模式:(下图是 混合模式)
什么是 热点检测?
JIT (即时编译) 中讲到【当JVM发现某个方法或代码块运行时执行的很频繁,就会认为是"热点代码(Hot Spot Code)"】。目前,主要识别 热点代码的方式是 热点检测(Hot Spot Detection)。
-
基于采样的方式探测(Sample Based Hot Spot Detection):周期性检测各个线程的栈顶,发现某个方法经常出现在栈顶,就认为是热点方法。
好处就是简单;缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。
-
基于计数器的热点探测(Counter Based Hot Spot Detection):虚拟机会为每个方法、代码块建立计数器,统计方法的执行次数,某个方法超过阀值就认为是热点方法,触发JIT编译。
++在HotSpot虚拟机中使用的是第二种,基于计数器的热点探测++。因此它为每个方法准备了两个计数器:
- 方法调用计数器:记录方法被调用的次数;
- 回边计数器 :记录方法中的
for
或者while
运行的次数;
编译优化
JIT除了之前提到的可以将 字节码 翻译为 机器码 缓存起来。还会对代码做各种优化,比如:逃逸分析、锁消除、锁膨胀、方法内联、空值检查消除、类型检测消除、公共子表达式消除。
讲讲 逃逸分析?
答:逃逸分析 是 HotSpot虚拟机 的 server 编译器 中 JIT 优化的一个重要步骤。对象基于逃逸分析有三种状态:全局逃逸(Global Escape)、参数逃逸(Arg Escape)、无逃逸(No Escape)。不同的逃逸状态 会影响 JIT (即时编译) 的优化策略。
- 全局逃逸(Global Escape) :对象超出了方法或线程的范围,比如被存储在 静态字段 、或作为方法的返回值。
- 参数逃逸(Arg Escape):对象被作为 参数传递、或被参数引用,但在方法调用期间不会全局逃逸。
- 无逃逸(No Escape):对象可以被 标量替换,意味着它的内存分配可以从生成的代码中移除。
补充:
-
全局逃逸:新创建的
staticobject
就是全局逃逸。第二个函数中的sb
也全局逃逸,因为它作为方法的返回值了。javapublic class GlobalEscapeExample { private static object staticobject; public void globalEscape() { staticobject = new object(); //这个对象赋值给静态字段,因此它是全局逃逸的 } public static stringBuffer craetestringBuffer(String s1,string s2) { stringBuffer sb = new stringBuffer( ); sb.append(s1); sb.append(s2); return sb; }
-
参数逃逸:传递到
methodB
中的param
对象,发生了参数逃逸,因为他从methodA
中逃逸到了methodB
中。javapublic class ArgEscapeExample { public void methodA() { object localobject = new object(); methodB(localObject); // localObject作为参数传递,但不会从methodB中逃逸 } public void methodB(object param) { //在这里使用param }
-
无逃逸:
sb
没有发生逃逸,因为这个对象本身 并没有作为参数传递、也没有作为方法的返回值,也没有赋值给静态变量。javapublic static stringBuffer craetestringBuffer(String s1,string s2) { stringBuffer sb = new stringBuffer( ); sb.append(s1); sb.append(s2); return sb.toString(); }
逃逸状态 对 JIT 优化策略 的影响?
在java中,不同的 逃逸状态 会影响 JIT 的优化策略:
- 全局逃逸:由于对象可能被多个线程访问,全局逃逸的对象一般不适合进行栈上分配 或 其他内存优化。但是JIT 可能会进行其他类型优化(如 方法内联 或者 循环优化);
- 参数逃逸:对象虽然作为参数传递,但不会被方法外部的代码使用。因此JIT可以对这些对象进行优化(如:锁消除);
- 无逃逸:这是最适合优化的情况。JIT可以采取多种优化措施,比如在 站上分配内存、消除锁、标量替换;(这些优化可以显著提高性能,减少垃圾收集的压力)
下图是三种情况可以、不可以进行的优化手段:
逃逸分析 技术并不成熟?
答:是的,关于逃逸分析相关的援救很早就有了(1999年),但知道 JDK1.6才使用。
根本原因是,无法保证逃逸分析 的性能消耗 高于 它所带来的收益。因为逃逸分析自身也需要经过一系列复杂的分析,也是比较耗时的过程。
介绍什么是 锁消除?
动态编译:是指 JIT(Just-In-Time)编译器在程序运行时对 Java 字节码进行的即时编译。
在动态编译同步块时,JIT编译器可以借助 逃逸分析 来判断同步块所使用的锁对象 是否只能够被一个线程访问,而没有被发布到其他线程。
如果同步块所使用的 锁对象 通过分析被证实只能被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消这部分代码的同步。这个取消同步的过程就叫做同步省略,即锁消除。
例子:下面代码对hollis
这个对象进行加锁,但是该对象的生命周期只在f()
方法中,并不会被其他线程所访问。
java
public void f() {
object hollis = new object( );
synchronized(hollis) {
system.out.println( hollis);
}
}
所以,在JIT编译阶段会被优化掉,优化为:
java
public void f() {
object hollis = new object( );
system.out.println( hollis);
}
标量替换 & 栈上分配?
标量替换
-
**标量(Scalar)**是指一个无法再分解为更小数据的数据。比如Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
-
标量替换 :在JIT阶段,经过逃逸分析,发现一个对象不会被外界访问。那么JIT优化就会++将这个对象拆解成若干标量来替代++ ,这个过程就是标量替换。好处就是:不需要在堆内存上分配内存,节省堆内存空间。
标量替换的例子:
示例中,point
对象没有逃逸出alloc
方法,并且point
对象是可以被分解为标量的。所以在JIT优化时,point
对象不会被创建,而是直接使用两个标量int x, int y
来替代point
对象。
栈上分配
一般情况下,对象、数组元素 在内存中会被分配到 堆内存 上 。但是随着JIT编译器的日渐发展,JIT编译器在运行期间进行逃逸分析,来决定是否可以将对象的内存分配 从 堆 转化为 栈。
hotspot的栈上分配 其实就是通过前面我们说过的标量替换实现的!
Java中的对象一定在堆上分配内存吗?
答:不一定。在HotSpot虚拟机中,存在JIT优化的机制,JIT优化中可能会进行逃逸分析 ,当经过逃逸分析发现某一个局部对象没有逃逸到线程和方法外的话,那么这个对象就可能不会在堆上分配内存,而是进行栈上分配(也就是会 通过标量替换 ,将对象分解为 标量,在栈上分配)。
方法内联
方法内联是指:将一个方法的代码直接插入到调用它的地方,从而避免了方法调用的开销。这种优化对于小型且频繁调用的方法特别有用。
即时编译器(JIT)用它(方法内联)来提高程序的运行效率。
示例:
上面例子中,被调用方法add()
内部逻辑非常简单。因此在JIT优化期间,可能会将add
方法直接内敛在 调用处,避免了对add
方法的实际调用。从而减少调用开销。
JIT优化可能带来的问题
一句话总结的可能带来的问题:就是JIT在优化之前,都是由解释器执行的代码,此时如果请求量大则会出现请求超时的问题。
JIT开始优化之前:JIT优化是在运行期进行的,并且也不是Java进程刚一启动就能优化的,是需要先执行一段时间的,因为他需要先知道哪些是热点代码。
-
所以,在IT优化开始之前,我们的所有请求,都是要经过解释执行的,这个过程就会相对慢一些。
-
而且,如果应用的请求量比较大的的话,这种问题就会更加明显,在应用启动过程中,会有大量的请求过来,这就会导致解释器持续的在努力工作。解释器一旦对CPU占用较高,就会导致应用的性能进一步下降。
这也是为什么很多应用在发布过程中,会出现刚重启的应用会发生大量的超时。
同时,随着请求不断增多,JIT优化会被触发。优化之后,后续的热点请求的执行就不再通过解释器了,而是直接通过JIT优化后的缓存机器码。这样就会快很多。
如何解决 JIT优化可能带来的问题?
现在已经知道JIT优化可能带来的问题:JIT在优化之前,都是由解释器执行的代码,此时如果请求量大则会出现请求超时的问题。
解决该问题有两种思路:
-
提升JIT的优化效率:比如可以使用阿里研发的JDK -- Dragonwell 。这个相比OpenJDK提供了一些专有特性,其中一项叫做
JwarmUp
的技术就是解决JIT优化效率的问题的。这个技术主要是通过记录Java应用上一次运行时候的编译信息到文件中,在下次应用启动时,读取该文件,从而在流量进来之前,提前完成类的加载、初始化和方法编译,++从而跳过解释阶段,直接执行编译好的机器码++。
-
降低瞬时请求量:除了针对JDK做优化之外,还可以采用另外一种方式来解决这个问题,那就是做预热。在程序刚启动时,通过调节负载均衡,不要很快的把大流量分给它,而是先给其一小部分流量,通过这部分流量出发JIT优化。等优化好之后,再把流量调大。
对JDK进程执行kill -9有什么影响?
答:kill -9
实际上是向该进程发送了SIGKILL
信号,该命令会立刻终止进程,而不会给进程任何机会进行清理工作或执行任何终止前的代码。
因此会造成:
- 导致数据丢失或损坏 :如果Java进程正在处理数据(如写入文件或数据库),
kill -9
可能会导致数据处理中断,从而造成数据丢失或损坏。 - 不会执行清理代码 :Java程序中可能有一些清理资源的代码(如关闭文件、网络连接、释放锁等),这些代码通常会在JVM正常关闭时执行。使用
kill -9
会跳过这些清理步骤,可能导致资源泄漏或数据不一致。 - 不会执行 Shutdown Hook :在Java中,你可以通过
Runtime.addShutdownHook
添加Shutdown Hook,这些Hook通常用于执行一些需要在JVM关闭时执行的操作。kill -9
不会触发这些Hook的执行。
kill -9 与 kill -15 的区别
注意:kill 命令默认的信号就是15。
kill -15
:会发送一个SIGTERM
的信号给对应的程序。当应用程序接收到该信号时,具体如何处理,应用程序可以自己决定。可选择的有:
- 立即停止程序
- 释放响应资源后停止程序
- 忽略该信号,继续执行程序
也就是说,kill -15
信号,只是通知对应的进行要进行"安全干净的退出",程序进程接收到该信号之后,会先进性一些准备工作(如释放资源、临时文件清理),然后在进行程序的终止。
所以,相比于kill -15
命令,kill -9
在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。