方法内联通过将小方法体直接展开到调用点,彻底消除栈帧分配、参数传递、跳转指令及上下文恢复等运行时开销,并为常量传播等后续优化创造条件,但仅对jit识别的热点方法生效。

因为方法调用本身不是“免费”的——每次调用都要分配栈帧、压入参数、保存返回地址、跳转执行、再弹出栈帧、恢复上下文。而内联把方法体直接展开到调用点,彻底绕过了这一整套流程。
方法调用实际发生的开销在哪
Java 方法调用在字节码层面表现为 invokestatic、invokevirtual 等指令,但真正耗时的是 JVM 在运行时为它做的配套工作:
- 为每个调用新建一个栈帧:包含局部变量表、操作数栈、动态连接、返回地址等结构
- 参数和返回值需在调用方与被调方之间拷贝或传递(尤其涉及对象引用或装箱类型时)
- CPU 需要执行跳转指令(如
call),破坏指令流水线,可能引发分支预测失败 - 栈帧频繁创建/销毁会增加 GC 压力(尤其当方法内创建短生命周期对象时)
内联如何从根源上消除这些开销
内联不是“加速调用”,而是让“调用”这个动作消失:
- 没有新栈帧:所有变量复用原方法的栈帧空间,零额外内存分配
- 无跳转指令:代码顺序执行,CPU 流水线更稳定,缓存局部性更好
- 参数直接代入:比如
add(10, 20)内联后变成10 + 20,连参数传递步骤都省了 - 为后续优化打开大门:常量传播(
10 + 20 → 30)、死代码消除、公共子表达式提取等,都依赖于代码被展开在同一作用域
为什么只对“热点方法”做内联
内联是 JIT 编译器在方法已被执行足够多次(达到阈值)后才触发的决策,原因很实在:
- 冷方法调用少,开销占比小,不值得投入编译资源
- JIT 需要运行时 profiling 数据(如调用计数、分支走向)来判断是否安全内联
- 盲目内联所有小方法会导致机器码体积膨胀,挤占 CPU 指令缓存(i-cache),反而拖慢整体性能
内联效果不是看字节码,而是看最终执行流
很多人用 javap -c 发现仍有 invokevirtual,就以为没内联——这是误解。字节码永远不变,内联发生在 JIT 编译后的本地机器码阶段。真正起作用的是:
- JIT 日志中出现 inline (hot) xxx::method
- JMH 基准测试中,相同逻辑在充分预热后吞吐明显提升、延迟下降
- 通过
-XX:+PrintOptoAssembly观察汇编输出,确认调用指令已被展开为连续计算指令











