这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 方法调用是 Java 多态和模块化的基础,也是 JIT 优化的关键边界。调用是否可以内联、虚方法有几个实现、lambda 如何编译、异常是否在热路径,都会影响峰值性能。
8.1 调用指令
| 指令 | 场景 |
|---|---|
| invokestatic | 静态方法 |
| invokespecial | 构造器、private、super |
| invokevirtual | 实例方法 |
| invokeinterface | 接口方法 |
| invokedynamic | 动态调用点 |
示例:
public class CallDemo {
public void run() {
staticCall();
virtualCall(this);
interfaceCall(new Runnable() {
public void run() {
System.out.println("task");
}
});
}
private static void staticCall() {}
private void virtualCall(Object object) {}
private static void interfaceCall(Runnable task) {
task.run();
}
}
查看:
javap -c CallDemo
8.2 静态分派与动态分派
静态分派:
void print(String value) {}
void print(Integer value) {}
print("java");
编译期根据静态类型选择重载方法。
动态分派:
Service service = new OrderService();
service.execute();
运行期根据实际对象类型选择实现。
| 类型 | 时机 |
|---|---|
| 重载 | 编译期静态匹配 |
| 覆写 | 运行期动态分派 |
8.3 内联
内联把方法体复制到调用点:
caller()
-> callee()
变为:
caller()
-> callee 逻辑
收益:
- 减少调用开销;
- 跨方法优化;
- 消除无法逃逸的对象;
- 支持锁消除;
- 提高缓存局部性。
限制:
- 方法过大;
- 异常处理复杂;
- 虚方法实现过多;
- 字节码超过阈值;
- 类型信息不确定。
8.4 内联阈值
常见诊断参数:
-XX:MaxInlineSize
-XX:FreqInlineSize
-XX:+PrintInlining
查看:
java -XX:+PrintFlagsFinal -version | grep Inline
不建议先调阈值。优先:
- 用 profile 找热点;
- 检查方法是否过大;
- 检查接口实现数量;
- 拆小热点方法;
- 用 JMH 验证。
8.5 多态与去虚拟化
interface Payment {
void pay();
}
如果运行时只有一个实现,JIT 可能激进内联。之后出现第二个实现,会触发去优化。
Mono / Bimorphic
-> 内联机会较好
Megamorphic
-> 通常走常规虚调用
生产意义:
- 压测输入要覆盖真实类型分布;
- 抽象层次不一定必然导致慢;
- 框架动态代理可能增加调用层;
- 先测再优化;
- 保持小方法比手工写大方法更有利于优化。
8.6 Lambda 与方法引用
int total = orders.stream()
.mapToInt(Order::amount)
.sum();
Lambda 常通过 invokedynamic 创建调用点,不需要为每个 lambda 显式生成匿名类,具体机制与 JDK 版本有关。
方法引用:
Function<String, Integer> parser = Integer::parseInt;
性能建议:
- 避免在超热循环中重复创建捕获 lambda;
- 复用无状态函数对象;
- 不要为了性能牺牲可读性;
- 用 JMH 验证;
- 关注自动装箱。
8.7 反射与 MethodHandle
反射:
Method method = Order.class.getMethod("id");
Object value = method.invoke(order);
优化:
method.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.unreflected(method);
现代 JDK 对反射有优化和缓存,但高频调用仍可能带来额外开销。框架常见做法:
- 缓存 Method;
- 字节码生成代理;
- MethodHandle;
- LambdaMetafactory;
- 编译期生成代码。
8.8 动态代理
JDK Proxy:
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class<?>[]{Service.class},
(proxyObj, method, args) -> {
long start = System.nanoTime();
try {
return method.invoke(new OrderService(), args);
} finally {
long cost = System.nanoTime() - start;
System.out.println(method.getName() + " cost=" + cost);
}
});
成本:
- 多层调用;
- 装箱;
- 反射;
- 栈深增加;
- 参数数组分配。
生产排查要能关闭或采样增强逻辑。
8.9 异常与调用成本
问题示例:
try {
int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
return 0;
}
异常作为控制流的成本来自:
- 填充栈;
- 构造对象;
- 捕获处理;
- 影响 JIT 优化;
- 日志放大。
高频路径应先校验,或返回结果对象。
8.10 内联诊断
PrintInlining:
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintInlining \
-jar app.jar
输出示例:
inline (hot)
com.example.OrderService.calculate(int)
常见标记:
| 标记 | 含义 |
|---|---|
| inline | 已内联 |
| too large | 方法过大 |
| hot method too big | 热点方法过大 |
| not inlinable | 无法内联 |
| megamorphic | 多实现调用 |
生产建议先用 async-profiler 火焰图确认热点,再在测试环境打开诊断参数。
本章小结
方法调用指令决定分派方式,JIT 内联是跨方法优化的基础。小方法、稳定的类型分布和低异常频率通常更利于优化。接口、动态代理和反射不一定必然慢,但需要用 profile 和基准测试验证。
思考题
invokevirtual和invokeinterface的差异是什么?- 内联有哪些收益和限制?
- 什么是 megamorphic call?
- Lambda 与匿名类在实现上有什么不同?
- 为什么异常不适合作为高频控制流?