方法内联通过消除调用开销(如压栈、跳转、查vtable)和释放后续优化机会(如常量传播、死代码消除、逃逸分析)双路径提升效率,是JIT进一步优化的前提而非终点。

方法内联不是“开了就快”,而是通过消除调用开销 + 释放后续优化机会,双路径提升执行效率。它本身不提速,但为提速铺路。
方法调用开销被直接抹掉
每次普通方法调用要压栈帧、保存寄存器、跳转、返回、弹栈——哪怕一个 getFoo() 这样的 trivial getter,也得走完整流程。内联后,JVM 把 getFoo() 的字节码直接“贴”进调用处,只剩一次字段读取(如 return this.foo;)。实测在热点循环中,这能减少 10%~20% 的指令数和分支预测失败率。
-
invokevirtual调用在未内联时需查虚方法表(vtable),而内联后连查表动作都省了 - 小方法(如
Objects.requireNonNull()、Math.min())默认就满足内联条件,无需手动改写 - 注意:
invokedynamic(如 Lambda)是否内联取决于具体引导方法实现,不保证
触发更激进的跨方法优化
内联不是终点,是 JIT 编译器做进一步优化的起点。只有代码“合并在一块”,才能做常量传播、死代码消除、循环融合等操作。
- 比如
bar(flag)被内联进foo()后,JIT 发现flag是static final boolean,直接把整个分支折叠成return value0; - 若未内联,JIT 只能在各自方法内优化,无法跨边界推理
- 内联后,逃逸分析更容易判定对象未逃逸,进而触发栈上分配或标量替换
哪些方法容易被内联?看三个硬门槛
JVM 不是见小就内,它靠三道关卡筛选:
-
-XX:MaxInlineSize:默认 35 字节(非源码行数,是字节码长度)。超过就不考虑,除非是@ForceInline(JDK 16+)或private/final/static方法可放宽到-XX:FreqInlineSize(默认 325 字节) -
-XX:CompileThreshold:方法需被调用足够多次(C2 默认 10000 次)才进入编译队列,内联决策发生在编译阶段,不是解释阶段 - 多态确定性:对
public方法,JIT 必须能证明运行时只有一种实现(如类被-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining显示inline (hot)或did not inline (inlining too deep))
调试内联是否生效,别靠猜
光看代码大小或命名没用,必须用 JVM 自带诊断输出验证:
- 加参数:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+PrintCompilation - 观察日志里有没有类似
inline (hot)或inlined (hot)的标记,以及目标方法是否出现在Compiled method (c2)的字节码 dump 中 - 常见误判点:方法刚启动时还在解释执行,
PrintInlining不会输出任何内容;必须等 JIT 编译完成(通常几秒到几十秒,取决于负载) - 如果看到
too big,说明超MaxInlineSize;看到not inlineable,大概率是虚调用且未单实现化
真正影响性能的,往往不是某次内联与否,而是内联后能否引发连锁优化。比如一个 final 工具方法被内联 10 次,可能带来 5 处常量折叠和 2 次数组边界检查消除——这些才是吞吐量跃升的关键。别只盯着“有没有内联”,要看“内联之后 JIT 干了什么”。











