内联缓存是JVM为加速invokevirtual指令的虚方法调用而自动维护的运行时结构,它通过缓存“上次调用的对象类型→方法入口”映射实现快速跳转,避免重复查vtable;单态内联缓存最高效,但仅在调用点类型稳定时生效,final/private/static方法不涉及该机制。

什么是内联缓存,它和 invokevirtual 有什么关系
内联缓存(Inline Cache)不是 Java 代码里能直接写的语法或 API,而是 JVM 在执行 invokevirtual 指令时,为加速虚方法查找而自动维护的运行时结构。每次调用一个非 final 的实例方法,JVM 都要根据对象实际类型去查虚方法表(vtable),这个过程如果不优化,开销不小。内联缓存就是把「上一次调用的对象类型 → 对应方法入口」这个映射记下来,下次若类型一致,就跳过查表,直接跳转。
它不改变语义,只影响性能;你写 animal.makeSound(),编译后就是 invokevirtual,JVM 自动决定是否启用、以及用哪种形态的内联缓存(单态 / 多态 / 超多态)。
单态内联缓存最常见,但前提是调用点“稳定”
所谓“单态”,指某个 invokevirtual 调用点(即字节码中某条指令位置)在运行时几乎总是面对同一子类实例。比如:
Animal a = new Dog(); a.makeSound(); // ← 这个调用点,后续循环中一直用 Dog 实例
这时 JVM 会把它标记为单态,并缓存 Dog.class → Dog.makeSound 的映射。下一次调用直接跳过去,等效于静态绑定。
- 如果后续突然传入
new Cat(),缓存失效,触发“去优化”(deoptimization),降级为多态缓存或退回到查 vtable - final 方法、private 方法、static 方法不会走这套机制——它们压根不用
invokevirtual - 接口方法(
invokeinterface)也有类似缓存,但实现更复杂(需查 itable),命中率通常低于单态 invokevirtual
怎么观察内联缓存是否生效
不能靠肉眼,得靠 JVM 的诊断输出。启动时加这些参数:
-XX:+PrintInlining -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
重点关注日志里是否出现:
-
inline (hot):说明该方法被 JIT 内联了(注意:这是方法内联,和内联缓存不同,但常伴随发生) -
monomorphic或monomorphic call:明确告诉你这个 invokevirtual 是单态的,内联缓存已启用 -
megamorphic:表示该调用点见过太多类型(>4),缓存失效,每次都要查表,是性能隐患信号
别依赖 -XX:+PrintCompilation,它只告诉你哪些方法被编译了,不反映缓存状态。
为什么有些多态调用就是快不起来
内联缓存的效果高度依赖运行时 profile 数据。以下情况会让它失效或降级:
- 刚启动、未进入热点(JIT 还没收集足够类型信息),所有 invokevirtual 都走慢路径
- 一个调用点混用多个子类(如
list.forEach(a -> a.makeSound())里既有Dog又有Cat),容易滑向多态甚至超多态 - 使用反射或
Method.invoke(),完全绕过内联缓存机制 - 开启
-XX:-UseInlineCaches(极少见,仅用于调试),强制禁用
真正难处理的不是“能不能用内联缓存”,而是“它在什么条件下悄悄失效”。线上服务跑几天后变慢,有时就因为某个关键调用点从 monomorphic 退化成了 megamorphic,而代码本身没改过一行。

















