⭐ 推荐:社区规则条款 V1.0

Java 的 JIT 编译?

域名注册
域名注册 版主萌新站长 👑黑卡会员
发布于 2026-10-19 18:38 ·3 浏览 ·0 回复
内容摘要

解释 HotSpot JVM 的 JIT 即时编译机制,包括热点判定阈值、OSR、C1/C2 分层编译、去优化及用 PrintCompilation 观察编译结果。

JIT(Just-In-Time,即时编译)是 HotSpot JVM 默认开启的运行时优化机制:它把反复执行的热点字节码编译成本地机器码,让 Java 跑出接近 C 的性能。你不需要手动打开它,但必须知道它的触发阈值、观察方式和失效场景,否则线上“预热慢”“跑一会儿才快”这类问题没法解释。

JIT 编译到底做了什么?

结论:JIT 不是把整个程序提前编译,而是只挑“跑得够多”的方法和循环,在运行时编译并替换执行入口。

JVM 启动时先用解释器执行字节码,同时为每个方法维护两个计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,统计循环体跳回的次数)。计数超过阈值后,这个方法就被提交给编译线程编译成机器码,下一次调用直接跳到编译结果上执行。循环体单独触发编译的机制叫 OSR(On-Stack Replacement,栈上替换),作用是在方法还没返回时就切到编译后的代码,避免一个长循环一直解释执行。

编译后做的主要优化有:方法内联(把被调用者代码嵌进调用者,小方法默认内联)、常量折叠、死代码消除、循环展开、逃逸分析(Escape Analysis,判断对象是否逃出方法或线程),以及由此衍生出的标量替换——把不逃逸的对象拆成几个局部变量,直接分配在栈上,省掉一次堆分配。

热点代码的判定阈值是多少?

结论:不开分层编译时,Server 模式方法调用阈值是 10000,Client 模式是 1500;开了分层编译(Java 8 起默认开启)后,实际生效的是 Tier 3 的 200 次和 Tier 4 的 15000 次。

  • -XX:CompileThreshold=10000:Server 模式默认值,Client 模式为 1500。
  • 分层编译下,C1 在约 200 次调用后介入做 profiling,C2 在约 15000 次调用后接手做深度优化。
  • OSR 编译阈值 = CompileThreshold × OnStackReplacePercentage / 100,Server 模式 OnStackReplacePercentage 默认 140,即约 14000 次循环回边。

这也解释了一个常见现象:同一段代码,前几千次调用是解释执行加 C1,性能只到峰值的几成,跑到一万多次之后才被 C2 优化到最终形态。

C1 和 C2 有什么区别?分层编译在做什么?

结论:C1 编译快、优化浅,C2 编译慢、优化激进;分层编译就是把两者串成一条流水线,先保证快速启动,再逐步换上高性能代码。

HotSpot 内置两个 JIT 编译器:C1(Client Compiler)编译耗时短,只做局部优化;C2(Server Compiler)会做逃逸分析、激进内联等全局优化,编译一次要几十到上百毫秒。分层编译共分 5 个层级:0 层解释执行、1 层 C1 无 profiling、2 层 C1 有限 profiling、3 层 C1 全 profiling、4 层 C2。代码通常按 0 → 3 → 4 的路径升级,收集到的类型分布数据(是否单态调用)直接决定 C2 敢不敢做内联。

如果 C2 基于 profiling 做的乐观假设被打破——比如某个调用点原本只见过一种类型,突然来了第二种——会触发去优化(Deoptimization),代码回退到解释执行重新收集信息。这是正常机制,不是 bug。

怎么确认 JIT 编译了哪些方法?

结论:用 -XX:+PrintCompilation 看实时编译事件,用 -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 看内联决策,用 -XX:+LogCompilation 输出完整日志再交给 JITWatch 分析。

java -XX:+PrintCompilation -jar app.jar
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -jar app.jar
java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=jit.log -jar app.jar

PrintCompilation 每行输出包含编译序号、时间戳、编译层级(1-4 或带 % 表示 OSR、s 表示 synchronized、! 表示有异常处理器)、方法名。另外可以关注代码缓存(Code Cache,存放编译结果的本地内存区),大小由 -XX:ReservedCodeCacheSize 控制,64 位分层编译默认 240MB;一旦写满,JIT 会停止编译并打印 “CodeCache is full”,性能随之骤降。

为什么我的基准测试结果不可信?

结论:没预热到 C2 的测试,测的是解释器和 C1 的性能,不能代表线上表现。

写微基准必须用 JMH(Java Microbenchmark Harness),它内置多轮预热、@Warmup/@Measurement 迭代控制和死代码消除防护。用 System.currentTimeMillis() 包一个空循环手写测试,JIT 会把整个循环优化掉,测出来的数字毫无意义。线上服务同理,如果流量是缓慢爬升的,压测时至少让目标方法经历过 15000 次调用再开始采样。

另外,JIT 只优化 CPU 侧,不解决启动慢的根因。如果启动时间是硬指标,可以看 AOT 方案:Java 9 的 jaotc 已在 Java 17 移除,当前可行的是 GraalVM Native Image,把编译动作前移到构建期,代价是失去运行时的动态优化和部分反射能力。

一句话收束:JIT 的核心是“计数器触发 + 分层编译 + 数据驱动优化”,掌握 CompileThreshold、分层路径、PrintCompilation 和代码缓存这四件事,就能解释绝大多数“预热后变快”和“跑久了变慢”的现象。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2300.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~