-xx:compilecommand可按方法粒度控制jit编译,支持compileonly、exclude等指令,需严格书写完整签名(如java/lang/string.tostring()ljava/lang/string;),双冒号语法适用于java 10+,内部类用$分隔,多次设置同名方法以最后为准。

怎么用 -XX:CompileCommand 精确控制某个方法是否被JIT编译
Java的JIT编译不是全有或全无,而是可以按方法粒度干预。关键就是 -XX:CompileCommand,它支持 compileonly、exclude、inline、dontinline 等指令。
常见错误是写错方法签名:必须包含完整类名(带包)、方法名、参数类型描述符(如 java/lang/String.toString()Ljava/lang/String;),漏掉分号或括号会静默失效。
- 排除一个方法不被编译:
-XX:CompileCommand=exclude,java.lang.String::toString(注意双冒号,Java 10+ 支持;旧版用点号+括号) - 只编译特定方法:
-XX:CompileCommand=compileonly,com.example.Service::process,其他方法即使热也会跳过C2编译 - 路径中含特殊字符(如
$)要转义,内部类写成Outer$Inner::method,不能写Outer.Inner - 该参数可多次出现,顺序无关,但同名方法后写的会覆盖前写的(比如先
exclude后compileonly,最终以compileonly为准)
-XX:+PrintCompilation 输出里哪些行代表内联实际发生了
打开 -XX:+PrintCompilation 只能看到方法被编译了,但无法直接看出内联是否成功。真正反映内联的是 -XX:+PrintInlining,它会在编译日志中标出每一层调用是否被内联、为什么失败。
典型输出像:! 123 com.example.Helper::calc (15 bytes) inline (hot) 表示已内联;而 @ 15 com.example.Utils::validate (22 bytes) failed to inline: hot method too big 就说明因字节码过大被拒。
立即学习“Java免费学习笔记(深入)”;
- 必须配合
-XX:+UnlockDiagnosticVMOptions才能启用-XX:+PrintInlining - 内联决策发生在C2编译阶段,所以只有被C2编译的方法才会触发内联日志(C1编译的方法不会显示)
- 如果某方法在
PrintInlining中完全没出现,大概率是它压根没被C2选中编译——先检查它是否够“热”,或是否被exclude了
内联排除后,方法仍可能被解释执行甚至C1编译,这不是bug
用 exclude 或 dontinline 并不等于“禁用该方法”。它只影响JIT编译器的决策,不影响解释器和C1编译器的行为。
例如对一个高频小方法加了 -XX:CompileCommand=dontinline,com.example.Cache::get,它仍可能被C1编译(因为C1默认更激进内联),也可能长期停留在解释执行状态——只要没达到C2触发阈值。
- C1和C2使用不同的内联策略和阈值,
dontinline只对C2生效(除非显式用-XX:TieredStopAtLevel=1关闭C2) - 排除编译后,方法调用开销会上升:从直接跳转变成查虚表/解释栈帧切换,尤其在循环体内明显
- 若想彻底阻止某方法被任何编译器处理,得组合使用:
exclude+-XX:TieredStopAtLevel=1(停在C1)+-XX:CICompilerCount=1(只启一个C1线程)
生产环境慎用内联干预,调试时容易误判“优化有效”
看到 PrintInlining 里某方法被成功内联,不代表性能一定变好。过度内联可能增大代码缓存压力、拖慢编译速度、甚至因寄存器溢出导致生成更差的汇编。
真实瓶颈常在IO、锁竞争或GC,而非单个方法是否内联。强行排除一个看似“可疑”的方法,可能掩盖真正的热点转移路径。
- 用
jstack或async-profiler确认热点在哪儿,再决定是否干预,别只盯着编译日志里的“failed to inline” - JDK 17+ 的
-XX:+UseJVMCICompiler(GraalVM EE)会改变内联策略,原有CompileCommand规则不一定适用 - 容器环境下,
-XX:ReservedCodeCacheSize偏小会导致编译队列积压,此时即使写了compileonly,方法也可能迟迟不被编译










