“代码跑着跑着,就变快了?”——揭秘Java性能幕后引擎:即时编译器

HotSpot虚拟机内部集成了两个即时编译器,分别被称为C1编译器(Client Compiler/ Quick Complier)和C2编译器(Server Compiler)。自Java 9起,-server模式(即启用C2编译器或分层编译)是默认选项,-client选项通常会被忽略。

C1编译器的启动速度较快,主要关注局部的、简单且可靠的优化策略,例如方法内联、常量传播、死代码消除、冗余消除等。相比之下,C2编译器则专注于全局优化,这些优化通常需要更长的编译时间,甚至会根据性能监控(profiling)数据进行一些激进但不一定可靠的优化,例如更复杂的内联决策、逃逸分析、循环优化、向量化等。C2编译器的性能通常比C1编译器高出30%以上,因此更适合长时间运行的后台程序。

从Java 7开始引入,并在Java 8中成为默认策略(当C2可用时),分层编译结合了C1的快速启动和C2的高峰值性能。它将编译过程划分为5个层次。

1)第0层:解释执行收集性能监控数据,主要是方法调用计数器和循环回边计数器。

2)第1层:C1编译器(Simple C1)不进行Profiling,快速编译为本地代码。

3)第2层:C1编译器(Limited Profile C1)进行少量的Profiling(调用次数、循环次数)。

4)第3层:C1编译器(Full Profile C1)进行全面的Profiling,收集包括分支频率、类型信息等更详细的数据,为C2做准备。

5)第4层:C2编译器利用C1收集到的详尽Profiling数据,进行最大程度的优化编译。

性能监控是在程序执行过程中收集反映代码执行状态的数据,如方法调用频率、循环执行频率、分支跳转信息、类型剖面等。这些数据是即时编译器(尤其是C2)做出明智优化决策的依据。性能监控的精度越高,其带来的额外性能开销就越大。最基本的是方法调用计数器和循环回边计数器,用于识别热点代码并触发即时编译。编译阈值是动态的,并且受分层编译策略的影响,但传统的Client模式下默认阈值约为1500次调用,Server模式下约为10000次调用(这些具体数字可能随JDK版本和模式变化)。

方法调用计数器

方法调用计数器(Invocation counter),顾名思义,这个计数器就是用于统计方法被调用的次数。需注意该计数器统计的非绝对次数,而是衡量一个相对的执行频率。当超过一定的时间限度,如果方法的调用次数仍不足以触发即时编译,那这个方法的调用计数会被减少一半,这个过程称为热度的衰减 (Counter decay),而这段时间就称为此方法统计的半衰周期 (Counter half life time)。

java 复制代码
@RequestMapping(value = "/input")
public CommonResponse input(@RequestBody InputRequest request) {
     // 如果 input 方法本身成为热点,它会被JIT编译。
     // JIT编译器可能会决定将 doSomething 方法内联到 input 方法中,
     // 如果 doSomething 方法符合内联条件(如方法体小、调用频繁等)。
     return CommonResponse.ok(doSomething(request));
}
    
public void doSomething(InputRequest request) {
     // 如果 doSomething 方法自身被频繁调用(无论是直接调用还是通过 input 间接调用),
     // 并且达到了编译阈值,它也会被JIT编译成本地机器码。
     // ... 复杂的业务逻辑 ...
}

循环回边计数器

循环回边计数器(Loop backEdge counter)会对程序中的循环进行计数。每当程序执行一次循环的回边(即从循环的末尾跳回到循环的开始),循环回边计数器的值就会增加。

java 复制代码
void loop() {
    int sum = 0;
    for (int i = 0; i < 10; i++) {
        sum += i;
    }
}

上面这段代码经过编译生成下面的字节码:

java 复制代码
  public void loop();
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: bipush        10
       7: if_icmpge     20
      10: iload_1
      11: iload_2
      12: iadd
      13: istore_1
      14: iinc          2, 1
      17: goto          4
      20: return

在上述字节码中,循环回边计数器被存储在第7行的if_icmpge指令中。if_icmpge指令用于接收两个操作数用于比较计算,以决定循环体跳转的位置。在解释执行时,每当运行一次该指令,该方法的循环回边计数器加1。

循环回边计数器触发的优化编译技术叫作栈上替换 (On stack replacement,OSR) 。假设有一个方法只被调用一次,但却包含超过一万次以上循环迭代次数,这个循环方法无法以方法调用计数来统计。而栈上替换技术解决了这个问题。当编译器检测到一个循环已经迭代次数达到阈值时,动态地将这个循环(以及包含它的方法的一部分)编译成本地机器码,并让当前正在执行的线程"切换"到新编译的代码上继续执行循环,而无需等待方法调用结束。

java 复制代码
void largeLoop() {
   // 假设此方法只被调用一次
    long sum = 0;
    // 1. 循环回边计数器通过迭代统计,即使方法调用次数少,此循环也会变热。
    // 2. 当达到OSR阈值,JIT会将循环部分编译成本地机器码。
    // 3. 正在执行的线程会从解释执行(或C1代码)的循环"栈上替换"到新编译的C2代码。
    for (int i = 0; i < 100000000; i++) { // 非常大的循环次数
        sum += i;
        // ... 其他操作 ...
    }
    System.out.println(sum);
}

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!

相关推荐
达文汐22 分钟前
【困难】力扣算法题解析LeetCode332:重新安排行程
java·数据结构·经验分享·算法·leetcode·力扣
培风图南以星河揽胜23 分钟前
Java版LeetCode热题100之零钱兑换:动态规划经典问题深度解析
java·leetcode·动态规划
启山智软1 小时前
【中大企业选择源码部署商城系统】
java·spring·商城开发
我真的是大笨蛋1 小时前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怪兽源码1 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
恒悦sunsite1 小时前
Redis之配置只读账号
java·redis·bootstrap
梦里小白龙1 小时前
java 通过Minio上传文件
java·开发语言
人道领域1 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
sheji52612 小时前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Java Web的电子商务网站的用户行为分析与个性化推荐系统为例,包含答辩的问题和答案
java·开发语言