09.深入解密JVM invokedynamic机制——调用点与Lambda表达式实现(下)

图片[1]-09.深入解密JVM invokedynamic机制——调用点与Lambda表达式实现(下)-速优课

本文原创,转载请注明出处。关注博主,持续分享Java进阶知识。

一、开篇:invokedynamic的核心思想

上一篇我们深入探讨了方法句柄(MethodHandle),今天我们正式介绍invokedynamic指令——Java 7引入的动态方法调用机制。

核心思想: 将方法调用的链接过程从JVM内部暴露给应用程序,让应用程序自己决定调用哪个方法。

应用场景:

  • 动态语言实现:支持鸭子类型、动态分派
  • Lambda表达式:Java 8的Lambda借助invokedynamic实现
  • 方法引用:支持类型无关的方法调用

二、invokedynamic指令详解

2.1 指令结构

invokedynamic #80, 0 // race:(Ljava/lang/Object;)V

组成部分说明
#80指向常量池中引导方法的索引
0指向常量池中静态参数的索引(可选)
race目标方法名
(Ljava/lang/Object;)V方法描述符

2.2 执行流程

图片[2]-09.深入解密JVM invokedynamic机制——调用点与Lambda表达式实现(下)-速优课

2.3 引导方法(Bootstrap Method)

签名要求:

public static CallSite bootstrap(
    MethodHandles.Lookup lookup,  // 调用者的Lookup对象
    String name,                    // 目标方法名
    MethodType callSiteType,        // 调用点类型
    Object... args                  // 额外参数
) throws Throwable;

示例:

public static CallSite bootstrap(
    MethodHandles.Lookup l, 
    String name, 
    MethodType callSiteType
) throws Throwable {
    // 创建方法句柄
    MethodHandle mh = l.findVirtual(Horse.class, name, 
                                   MethodType.methodType(void.class));
    // 返回常量调用点
    return new ConstantCallSite(mh.asType(callSiteType));
}

2.4 调用点(CallSite)的类型

类型说明可重链接
ConstantCallSite链接固定的方法句柄
MutableCallSite链接可变的方法句柄
VolatileCallSite链接可变的方法句柄(volatile语义)

三、实战:实现单态内联缓存

3.1 问题场景

class Horse {
    public void race() { System.out.println("Horse.race()"); }
}
​
class Deer {
    public void race() { System.out.println("Deer.race()"); }
}
​
// 如何用同一种方式调用不同类型的race方法?
public static void startRace(Object obj) {
    // 需要invokedynamic指令
}

3.2 单态内联缓存实现

import java.lang.invoke.*;
​
public class MonomorphicInlineCache {
    private final MethodHandles.Lookup lookup;
    private final String name;
    
    public MonomorphicInlineCache(MethodHandles.Lookup lookup, String name) {
        this.lookup = lookup;
        this.name = name;
    }
    
    // 缓存的类型和方法句柄
    private Class<?> cachedClass = null;
    private MethodHandle mh = null;
    
    public void invoke(Object receiver) throws Throwable {
        // 类型检查和缓存更新
        if (cachedClass != receiver.getClass()) {
            cachedClass = receiver.getClass();
            mh = lookup.findVirtual(cachedClass, name, 
                                   MethodType.methodType(void.class));
        }
        mh.invoke(receiver);
    }
    
    // 引导方法
    public static CallSite bootstrap(
        MethodHandles.Lookup l, 
        String name, 
        MethodType callSiteType
    ) throws Throwable {
        MonomorphicInlineCache ic = new MonomorphicInlineCache(l, name);
        MethodHandle mh = l.findVirtual(MonomorphicInlineCache.class, 
                                       "invoke", 
                                       MethodType.methodType(void.class, Object.class));
        // 绑定缓存对象
        return new ConstantCallSite(mh.bindTo(ic));
    }
}

3.3 使用ASM生成invokedynamic指令

import org.objectweb.asm.*;
​
public class ASMHelper implements Opcodes {
    private static class MyMethodVisitor extends MethodVisitor {
        public final MethodVisitor mv;
        
        public MyMethodVisitor(int api, MethodVisitor mv) {
            super(api);
            this.mv = mv;
        }
        
        @Override
        public void visitCode() {
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            
            // 创建引导方法句柄
            Handle h = new Handle(
                H_INVOKESTATIC,
                "com/example/MonomorphicInlineCache",
                "bootstrap",
                MethodType.methodType(CallSite.class, 
                    MethodHandles.Lookup.class, 
                    String.class, 
                    MethodType.class
                ).toMethodDescriptorString(),
                false
            );
            
            // 生成invokedynamic指令
            mv.visitInvokeDynamicInsn("race", "(Ljava/lang/Object;)V", h);
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
    }
    
    // ... 省略ClassReader/ClassWriter逻辑
}

四、Java 8 Lambda表达式的实现

4.1 Lambda到函数式接口的转换

IntStream.of(1, 2, 3)
    .map(i -> i * 2)           // 无捕获变量
    .map(i -> i * x);          // 捕获变量x

转换过程:

  1. 解语法糖:编译器生成包含Lambda内容的方法
  2. invokedynamic:第一次执行时生成适配器类
  3. 适配器调用:调用生成的方法

4.2 Lambda解语法糖

无捕获变量的Lambda:

// i -> i * 2
private static int lambda$0(int i) {
    return i * 2;
}

有捕获变量的Lambda:

// i -> i * x(x为捕获的变量)
private static int lambda$1(int x, int i) {
    return x * i;
}

4.3 适配器类的生成

无捕获变量的适配器类:

final class LambdaTest$$Lambda$1 implements IntUnaryOperator {
    private LambdaTest$$Lambda$1() {}
    
    public int applyAsInt(int i) {
        return LambdaTest.lambda$0(i);  // 直接调用生成的方法
    }
}

有捕获变量的适配器类:

final class LambdaTest$$Lambda$2 implements IntUnaryOperator {
    private final int arg$1;  // 保存捕获的变量
    
    private LambdaTest$$Lambda$2(int arg$1) {
        this.arg$1 = arg$1;
    }
    
    // 静态工厂方法,每次调用新建实例
    private static IntUnaryOperator get$Lambda(int arg$1) {
        return new LambdaTest$$Lambda$2(arg$1);
    }
    
    public int applyAsInt(int i) {
        return LambdaTest.lambda$1(arg$1, i);  // 使用捕获的变量
    }
}

4.4 导出适配器类

使用以下参数可以导出Lambda生成的适配器类:

java -Djdk.internal.lambda.dumpProxyClasses=/path/to/output LambdaTest

五、Lambda与方法句柄的性能分析

5.1 无捕获变量的Lambda

// v6版本
import java.util.function.IntConsumer;

public class Test {
    public static void target(int i) {}
    
    public static void main(String[] args) {
        for (int i = 1; i <= 2_000_000_000; i++) {
            ((IntConsumer) j -> Test.target(j)).accept(128);
        }
    }
}

性能结果: 与直接调用几乎一致!

原因:

  1. invokedynamic绑定ConstantCallSite,JIT可以内联
  2. 适配器类实例被缓存,无需重复创建
  3. accept方法直接调用lambda$0,可被内联

5.2 有捕获变量的Lambda

// v7版本
import java.util.function.IntConsumer;

public class Test {
    public static void target(int i) {}
    
    public static void main(String[] args) {
        int x = 2;
        for (int i = 1; i <= 2_000_000_000; i++) {
            ((IntConsumer) j -> Test.target(x + j)).accept(128);
        }
    }
}

性能结果: 默认与直接调用一致!

原因: JIT的逃逸分析将适配器实例优化为栈分配或消除

关闭逃逸分析后的结果: 约为直接调用的2.5倍

java -XX:-DoEscapeAnalysis Test

5.3 方法句柄的性能优化

普通方法句柄调用(v8版本):

MethodHandle mh = l.findStatic(Test.class, "target", t);
mh.invokeExact(128);  // 性能约为直接调用的3.5倍

优化:声明为常量(v9版本):

static final MethodHandle mh;  // 声明为静态常量
static {
    mh = l.findStatic(Test.class, "target", t);
}
// 性能与直接调用一致!

关键: JIT需要将方法句柄识别为常量才能内联!


六、实战:方法句柄的性能对比

6.1 v10版本:自定义CallSite

public static class MyCallSite {
    public final MethodHandle mh;
    
    public MyCallSite() {
        mh = findTarget();
    }
}

private static final MyCallSite myCallSite = new MyCallSite();

public static void main(String[] args) throws Throwable {
    for (int i = 1; i <= 2_000_000_000; i++) {
        myCallSite.mh.invokeExact(128);
    }
}

6.2 v11版本:继承ConstantCallSite

public static class MyCallSite extends ConstantCallSite {
    public MyCallSite() {
        super(findTarget());
    }
}

private static final MyCallSite myCallSite = new MyCallSite();

public static void main(String[] args) throws Throwable {
    for (int i = 1; i <= 2_000_000_000; i++) {
        myCallSite.getTarget().invokeExact(128);
    }
}

6.3 思考问题

  1. v10和v11的性能差异是什么?
  2. 为什么会有这种差异?
  3. getTarget()方法做了什么?

七、总结

核心要点回顾

  1. invokedynamic指令:将方法调用链接过程暴露给应用程序
  2. 引导方法:第一次执行时生成调用点,返回CallSite实例
  3. 调用点类型:ConstantCallSite、MutableCallSite、VolatileCallSite
  4. Lambda实现
  • 无捕获变量:缓存适配器实例
  • 有捕获变量:每次新建实例(依赖逃逸分析优化)
  1. 性能优化:将方法句柄声明为常量,JIT可以内联

关键理解

  • invokedynamic是Java支持动态语言的基石
  • Lambda表达式的性能上限可以达到直接调用
  • 捕获变量的Lambda依赖逃逸分析消除实例创建开销
  • 方法句柄需要被识别为常量才能被JIT内联

写在最后

invokedynamic机制为Java带来了强大的动态调用能力,不仅支撑了Lambda表达式的实现,也为动态语言运行时提供了基础。深入理解其原理,对于编写高性能代码和理解Java生态至关重要。

如果觉得本文对你有帮助,欢迎点赞、收藏、关注!后续我会继续分享更多JVM相关的深度解析文章。

📚 关注我,一起深入探索Java虚拟机的奥秘!

© 版权声明
THE END
喜欢就支持一下吧
点赞12
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!