JIT(即时编译)技术

介绍一下JIT优化技术?

想要把高级语言转变成计算机认识的机器语言有两种方式,分别是编译和解释,虽然Java转成机器语言的过程中有一个步骤是要编译成字节码,但是,这里的字节码并不能在机器上直接执行。

JVM中内置了 解释器(interpreter),在运行时对字节码进行解释翻译成机器码,然后再执行。

解释器 的执行方式是 一边翻译、一边执行,因此执行效率较低。为了解决这个低效率问题,HotSppot 引入了JIT 技术(Just-In-Time,即时编译)。此时:

  • JVM仍然是通过解释器进行解释执行;
  • 但是当JVM发现某个方法或代码块运行时执行的很频繁,就会认为是"热点代码(Hot Spot Code)"。然后JIT 会把部分 热点代码 翻译为 机器码 并进行优化,同时会把翻译后的机器码缓存起来,以备下次使用。

介绍一下 JIT 的两种编译器?

HotSppot 虚拟机中内置了两个 JIT编译器:Client CompilerServer Compiler

  • 客户端编译器(Client Compiler):也被称为 C1 编译器。C1 编译器会对代码进行简单的优化,并专注于提高编译速度。
  • 服务端编译器(Server Compiler):也被称为 C2 编译器。它是为了长时间运行的服务器端应用程序而设计的,因此在启动时可能会比 C1 编译器慢。C2 编译器会进行更复杂的优化,如全局优化、内联等,以获得更高的运行效率。

HotSpot 虚拟机提供了哪三种运行模式?

HotSpot 虚拟机提供了三种运行模式,这些模式主要影响即时编译器(JIT)的行为与性能:

  1. 解释模式(Interpreted Mode) :即所有代码都解释执行,而不经过 JIT 编译。使用 -Xint 参数可以打开这个模式。

    优点:启动快速,没有编译延迟;缺点:运行效率相对较低;

  2. 编译模式(Compiled Mode):Java 字节码会由 JIT 编译器编译成本地机器码后执行。这通常包括 C1 和 C2 编译器,它们分别负责客户端编译和服务端编译。

    优点:比解释模式 运行效率高;缺点:启动时间较长,因为需要时间来编译代码。

  3. 混合模式(Mixed Mode):HotSpot 虚拟机的默认模式。在混合模式下,Java 虚拟机会同时使用解释器和编译器。刚开始运行时,字节码由解释器执行,随着程序的运行,热点代码(即执行频率较高的代码)会被 JIT 编译器编译成本地机器码。

    优点:结合了++解释模式的快速启动++ 和++编译模式的高效运行++。

    -Xint:强制虚拟机运行于解释模式。
    -Xcomp:强制虚拟机运行于编译模式。
    不指定或使用 -Xmixed:默认使用混合模式。

可以通过java -version命令查看运行模式:(下图是 混合模式)

什么是 热点检测?

JIT (即时编译) 中讲到【当JVM发现某个方法或代码块运行时执行的很频繁,就会认为是"热点代码(Hot Spot Code)"】。目前,主要识别 热点代码的方式是 热点检测(Hot Spot Detection)

  1. 基于采样的方式探测(Sample Based Hot Spot Detection):周期性检测各个线程的栈顶,发现某个方法经常出现在栈顶,就认为是热点方法。

    好处就是简单;缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。

  2. 基于计数器的热点探测(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也全局逃逸,因为它作为方法的返回值了。

    java 复制代码
    public 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中。

    java 复制代码
    public class ArgEscapeExample {
        public void methodA() {
            object localobject = new object();
            methodB(localObject); // localObject作为参数传递,但不会从methodB中逃逸
        }
        
        public void methodB(object param) {
            //在这里使用param
        }
  • 无逃逸:sb没有发生逃逸,因为这个对象本身 并没有作为参数传递、也没有作为方法的返回值,也没有赋值给静态变量。

    java 复制代码
    public static stringBuffer craetestringBuffer(String s1,string s2) {
        stringBuffer sb = new stringBuffer( );
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

逃逸状态 对 JIT 优化策略 的影响?

在java中,不同的 逃逸状态 会影响 JIT 的优化策略:

  1. 全局逃逸:由于对象可能被多个线程访问,全局逃逸的对象一般不适合进行栈上分配 或 其他内存优化。但是JIT 可能会进行其他类型优化(如 方法内联 或者 循环优化);
  2. 参数逃逸:对象虽然作为参数传递,但不会被方法外部的代码使用。因此JIT可以对这些对象进行优化(如:锁消除);
  3. 无逃逸:这是最适合优化的情况。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在优化之前,都是由解释器执行的代码,此时如果请求量大则会出现请求超时的问题。

解决该问题有两种思路:

  1. 提升JIT的优化效率:比如可以使用阿里研发的JDK -- Dragonwell 。这个相比OpenJDK提供了一些专有特性,其中一项叫做JwarmUp的技术就是解决JIT优化效率的问题的。

    这个技术主要是通过记录Java应用上一次运行时候的编译信息到文件中,在下次应用启动时,读取该文件,从而在流量进来之前,提前完成类的加载、初始化和方法编译,++从而跳过解释阶段,直接执行编译好的机器码++。

  2. 降低瞬时请求量:除了针对JDK做优化之外,还可以采用另外一种方式来解决这个问题,那就是做预热。在程序刚启动时,通过调节负载均衡,不要很快的把大流量分给它,而是先给其一小部分流量,通过这部分流量出发JIT优化。等优化好之后,再把流量调大。

对JDK进程执行kill -9有什么影响?

答:kill -9实际上是向该进程发送了SIGKILL信号,该命令会立刻终止进程,而不会给进程任何机会进行清理工作或执行任何终止前的代码

因此会造成:

  1. 导致数据丢失或损坏 :如果Java进程正在处理数据(如写入文件或数据库),kill -9可能会导致数据处理中断,从而造成数据丢失或损坏。
  2. 不会执行清理代码 :Java程序中可能有一些清理资源的代码(如关闭文件、网络连接、释放锁等),这些代码通常会在JVM正常关闭时执行。使用kill -9会跳过这些清理步骤,可能导致资源泄漏或数据不一致。
  3. 不会执行 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在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。

相关推荐
鱼跃鹰飞6 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
王佑辉7 小时前
【jvm】Major GC
jvm
阿维的博客日记7 小时前
jvm学习笔记-轻量级锁内存模型
jvm·cas·轻量级锁
曹申阳10 小时前
2. JVM的架构模型和生命周期
jvm·架构
琪露诺大湿11 小时前
JavaEE-多线程初阶(4)
java·开发语言·jvm·java-ee·基础·1024程序员节·原神
王佑辉14 小时前
【jvm】Full GC
jvm
九鼎科技-Leo14 小时前
C# 内存管理与对象生命周期在面向对象设计中的重要性
jvm·c#
王佑辉1 天前
【jvm】堆空间分代思想
jvm
为啥不能修改昵称啊1 天前
静态数据区,堆,栈
java·jvm·算法
救苦救难韩天尊2 天前
《JVM第7课》堆区
jvm