【JVM】深入JIT优化机制

1.JIT优化技术

在将高级语言转化为计算机可识别的机器语言时,常用的两种方式是编译和解释。Java在编译过程中,首先将代码编译成字节码。但是,字节码并不能直接在机器上执行。因此,JVM中内置了解释器(Interpreter),它在运行时将字节码逐行翻译成机器码并执行。

然而,解释器的执行方式是一边翻译,一边执行,导致执行效率较低。为了提高效率,HotSpot JVM引入了JIT(Just-In-Time)编译技术。

有了JIT技术后,JVM仍然通过解释器进行初始执行。但当JVM发现某个方法或代码块被频繁执行时,它将其标记为"热点代码"(Hot Spot Code)。JIT随后将这些热点代码编译为机器码,并进行优化。优化后的机器码被缓存起来,以便下次直接使用,从而显著提升执行效率。

2.热点检测

上面我们说过,要想触发JIT,首先需要识别出热点代码。目前主要的热点代码识别方式是热点探测,有以下两种

  1. 基于采样的方式探测: 周期性检测各个线程的栈顶,发现某个方法经常出现在栈顶,就认为是热点方法。好处就是简单,缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因千扰热点探测。

  2. 基于计数器的热点探测: 采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,某个方法超过阀值就认为是热点方法,触发JIT编译。

    在HotSpot虚拟机中使用的是第二种一一基于计数器的热点探测方法,因此它为每个方法准备了两个计数器: 方法调用计数器和回边计数器。

    方法计数器。顾名思义,就是记录一个方法被调用次数的计数器

    回边计数器。是记录方法中的for或者while的运行次数的计数器

3.编译优化

逃逸分析

  1. 全局逃逸:对象超出了方法或线程的范围,比如被存储在静态字段作为方法的返回值
java 复制代码
public class GlobalEscapeExample {
    private static object staticObject;
    
    public void globalEscape() {
        static object = new 0bject();// 这个对象赋值给静态字段,因此它是全局逃逸的
    }
    public static stringBuffer craetestringBuffer(string sl,string s2){
        StringBuffer sb =new stringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
}

如我们新建的staticObject就是全局逃逸的。以及下面的方法中的sb对象,也是全局逃逸的。

  1. 参数逃逸: 对象被作为参数传递或被参数引用,但在方法调用期间不会全局逃逸。
java 复制代码
public class ArgEscapeExample {
    public void methodA() {
        obiect localobject =new object();
		methodB(localobject);//localobject作为参数传递,但不会从methodB中逃逸
    }

    public void methodB(object param){
        //在这里使用param
    }
}

如传递到methodB中的param对象,就是发生了参数逃逸的。因为他从methodA中逃逸到了methodB中

  1. 无逃逸: 对象可以被标量替换,意味着它的内存分配可以从生成的代码中移除。
java 复制代码
public static string createstringBuffer(string s1,string s2) {
    stringBuffer sb = new stringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.tostring();
}

如上面的sb,就没有发生逃逸,因为这个对象本身没有作为参数传递,也没有被当做方法返回值,并没有赋值给静态变量。

在Java中,不同的逃逸状态影响JIT (即时编译器)的优化策略:

  1. 全局逃逸: 由于对象可能被多个线程访问,全局逃逸的对象一般不适合进行栈上分配或其他内存优化。但JIT可能会进行其他类型的优化,如方法内联循环优化

  2. 参数逃逸: 这种情况下,对象虽然作为参数传递,但不会被方法外部的代码使用。JIT可以对这些对象进行一些优化,例如锁消除

  3. 无逃逸: 这是最适合优化的情况。JIT可以采取多种优化措施,如在栈上分配 内存,消除锁甚至完全消除对象分配 (标量替换)。这些优化可以显著提高性能,减少垃圾收集的压力。

方法内联

方法内联是Java中的一个优化技术,即时编译器JIT用它来提高程序的运行效率。在Java中,方法内联意味着将一个方法的代码直接插入到调用它的地方,从而避免了方法调用的开销。这种优化对于小型且频繁调用的方法特别有用。

锁消除

锁消除是 JIT 编译器在编译期间通过分析代码的同步块,判断是否存在锁竞争的可能性。如果某个锁在多线程环境下不存在竞争,那么它就可以在生成的机器码中消除这些锁操作,以减少不必要的开销。

栈上分配

栈上分配的好处:

  1. 减少GC压力:对象分配在栈上,当方法执行完毕后,栈上的内存会自动释放,不需要垃圾回收(GC)来管理,从而减少了GC的压力。
  2. 提高性能:栈上的内存分配和释放非常高效,因为它只是对栈指针进行简单的移动操作,而堆上的内存管理相对复杂,需要垃圾回收器的参与。

Java中的对象一定在堆上分配内存吗?

不一定,在HotSpot虚拟机中,存在JIT优化的机制,JIT优化中可能会进行逃逸分析,当经过逃逸分析发现某个对象不会逃逸出当前方法(即它只在方法内部使用),那么这个对象就不会被分配到堆上,而是进行栈上分配

标量替换

标量 是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量 。相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换

相关推荐
NEFU AB-IN2 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海7 小时前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗8 小时前
JVM整理
jvm
echoyu.8 小时前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考1 天前
JVM中内存管理的策略
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z1 天前
【JVM】详解 线程与协程
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm
Sincerelyplz3 天前
【JDK新特性】分代ZGC到底做了哪些优化?
java·jvm·后端