直接调用与方法引用在总线级无差异,因JVM方法调用不触发物理总线操作;性能差异实为字节码指令(invokevirtual vs invokedynamic)、JIT内联能力及对象分配开销所致。

直接测“虚方法调用慢不慢”没意义,关键在于构造可比、可控、可解释的对比场景。JMH 能给出纳秒级差异,但前提是排除 JIT 优化干扰、统一执行路径、明确测量目标。
明确要测什么:区分三种调用语义
多态虚调用(如 interface.method() 或 parentRef.childMethod())和直接引用(如 concreteObj.method())的性能差异,本质不是“函数调用本身”,而是 JVM 在运行时如何解析和分派。你需要拆解为:
-
单实现无内联场景:接口只有一个实现类,但禁用内联(用
@Fork(jvmArgs = {"-XX:CompileCommand=exclude,*.method"})),测纯虚表查表开销 - 多实现热点场景:同一接口有 2–4 个实现,让 JIT 观察到类型分布后触发去虚拟化(devirtualization)或内联缓存(ICache),测实际运行时开销
- 强制单态场景:用 final 方法或具体类型引用,作为理论下限基准,与虚调用并列对比
构造干净可比的测试结构
所有被测方法必须做完全相同的事,且结果不可被 JIT 消除:
- 统一操作:比如都对一个
int字段做自增,返回值用Blackhole.consume()消费 - 统一对象生命周期:避免 GC 干扰,字段声明为
@State(Scope.Benchmark)静态或实例成员,预热期间就初始化好 - 统一调用链长度:虚调用和直接调用都封装在一层 wrapper 方法里,避免因方法嵌套深度不同引入额外开销
- 禁用无关优化:加
@Fork(jvmArgs = {"-XX:-UseBiasedLocking", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly"})可选辅助诊断
用 JMH 注解控制变量
核心是让 JIT 行为稳定、可复现:
-
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS):确保 JIT 完成多次编译,尤其观察是否从 interpreter → C1 → C2 升级 -
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS):取足够样本降低噪声 -
@Fork(3):每次测试重启 JVM,排除残留状态影响 -
@BenchmarkMode(Mode.AverageTime):测单次调用平均耗时(ns 级),比吞吐量更易看出微小差异 - 对虚调用组,可加
@Fork(jvmArgsAppend = {"-XX:TypeProfileWidth=100"})控制类型预测宽度,验证 JIT 分析精度影响
识别真实差异而非噪声
虚调用开销通常极小(常低于 1 ns),容易被测量误差掩盖。需关注:
- 看 标准差 / 均值比值:若 > 5%,说明环境波动大,需检查 CPU 频率锁定、关闭 Turbo Boost、隔离 CPU 核心
- 对比 相对变化:比如虚调用比直接调用慢 1.8 ns,但 baseline(空方法调用)是 0.9 ns,则净开销约 0.9 ns —— 这才是 JIT 无法消除的虚分派成本
- 结合
-XX:+PrintCompilation日志,确认两个 benchmark 是否被编译为相似的汇编(如都有 inlined call site,或虚调用确实保留了call virtual指令) - 换不同 JDK 版本(如 JDK 17 vs JDK 21)跑,观察现代 JVM 的去虚拟化能力提升幅度

















