写点什么

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

作者:milanyangbo
  • 2025-08-05
    浙江
  • 本文字数:2102 字

    阅读完需:约 7 分钟

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)。


@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)会对程序中的循环进行计数。每当程序执行一次循环的回边(即从循环的末尾跳回到循环的开始),循环回边计数器的值就会增加。


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


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


  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) 。假设有一个方法只被调用一次,但却包含超过一万次以上循环迭代次数,这个循环方法无法以方法调用计数来统计。而栈上替换技术解决了这个问题。当编译器检测到一个循环已经迭代次数达到阈值时,动态地将这个循环(以及包含它的方法的一部分)编译成本地机器码,并让当前正在执行的线程“切换”到新编译的代码上继续执行循环,而无需等待方法调用结束。


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);}
复制代码


未完待续


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

发布于: 刚刚阅读数: 4
用户头像

milanyangbo

关注

让世界知道我的存在 2018-03-05 加入

技术/人文, 互联网

评论

发布
暂无评论
“代码跑着跑着,就变快了?”——揭秘Java性能幕后引擎:即时编译器_编译原理_milanyangbo_InfoQ写作社区