JVMNotes

第 08 章:方法调用与内联

zjc 于 2026-01-08 发布

这是《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 逻辑

收益:

  1. 减少调用开销;
  2. 跨方法优化;
  3. 消除无法逃逸的对象;
  4. 支持锁消除;
  5. 提高缓存局部性。

限制:

  1. 方法过大;
  2. 异常处理复杂;
  3. 虚方法实现过多;
  4. 字节码超过阈值;
  5. 类型信息不确定。

8.4 内联阈值

常见诊断参数:

-XX:MaxInlineSize
-XX:FreqInlineSize
-XX:+PrintInlining

查看:

java -XX:+PrintFlagsFinal -version | grep Inline

不建议先调阈值。优先:

  1. 用 profile 找热点;
  2. 检查方法是否过大;
  3. 检查接口实现数量;
  4. 拆小热点方法;
  5. 用 JMH 验证。

8.5 多态与去虚拟化

interface Payment {
    void pay();
}

如果运行时只有一个实现,JIT 可能激进内联。之后出现第二个实现,会触发去优化。

Mono / Bimorphic
  -> 内联机会较好

Megamorphic
  -> 通常走常规虚调用

生产意义:

  1. 压测输入要覆盖真实类型分布;
  2. 抽象层次不一定必然导致慢;
  3. 框架动态代理可能增加调用层;
  4. 先测再优化;
  5. 保持小方法比手工写大方法更有利于优化。

8.6 Lambda 与方法引用

int total = orders.stream()
        .mapToInt(Order::amount)
        .sum();

Lambda 常通过 invokedynamic 创建调用点,不需要为每个 lambda 显式生成匿名类,具体机制与 JDK 版本有关。

方法引用:

Function<String, Integer> parser = Integer::parseInt;

性能建议:

  1. 避免在超热循环中重复创建捕获 lambda;
  2. 复用无状态函数对象;
  3. 不要为了性能牺牲可读性;
  4. 用 JMH 验证;
  5. 关注自动装箱。

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 对反射有优化和缓存,但高频调用仍可能带来额外开销。框架常见做法:

  1. 缓存 Method;
  2. 字节码生成代理;
  3. MethodHandle;
  4. LambdaMetafactory;
  5. 编译期生成代码。

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);
            }
        });

成本:

  1. 多层调用;
  2. 装箱;
  3. 反射;
  4. 栈深增加;
  5. 参数数组分配。

生产排查要能关闭或采样增强逻辑。

8.9 异常与调用成本

问题示例:

try {
    int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
    return 0;
}

异常作为控制流的成本来自:

  1. 填充栈;
  2. 构造对象;
  3. 捕获处理;
  4. 影响 JIT 优化;
  5. 日志放大。

高频路径应先校验,或返回结果对象。

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 和基准测试验证。

思考题

  1. invokevirtualinvokeinterface 的差异是什么?
  2. 内联有哪些收益和限制?
  3. 什么是 megamorphic call?
  4. Lambda 与匿名类在实现上有什么不同?
  5. 为什么异常不适合作为高频控制流?