JVM字节码进阶

笔记参考书籍《深入理解JVM字节码》

方法调用指令

invokestatic

invokestatic,功能如其名,用来调用静态方法。调用的方法在编译期间确定,且运行期不会修改。且,调用本方法不需要将对象加载到操作数栈,只需要将所需要的参数入栈。

Integer.valueOf("42");

// 字节码
0: ldc #2
2: invokestatic #3 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer

invokevirtual

之前频繁使用的指令。用于调用普通实例方法,调用的目标方法在运行时才能根据对象实际的类型确定,在编译期间无法获知

调用 invokevirtual 指令之前,需要将 对象引用、方法参数入栈,结束后这些部分都会出栈。有返回值的方法会将结果入栈。

因为是虚方法分派,所以会根据传入对象的实际类型进行方法分派,在运行时动态选择执行具体子类的方法。

invokespecial

用来调用特殊的实例方法:

  • 实力构造器方法<init>
  • private 修饰的私有实例方法
  • 使用 super 关键字调用的父类方法

invokespecial 调用的方法可以在编译期间确定,private 方法不会因为继承被子类覆写,在编译期间就可以确定,所以 private 方法的调用使用 invokespecial。

invokeinterface

用于调用接口方法,与 invokevirtual 类似,在运行时根据对象的类型确定目标方法。(接口本身的方法没有实现怎么调用)

方法分配原理

涉及到 invokevirtual 和 invokeinterface 两个指令之间的区别,明确 invokevirtual 依赖于单继承。子类的虚方法保留父类的方法表顺序,但是涉及多接口实现时,就无法使用,需要 invokeinterface 指令。

C++ 方法分派设计

Java 受 C++ 方法分派的影响,也使用了类似的结构。

class A {
  public void method1() {}
  public void method2() {}
  public void method3() {}
}

class B extends A {
  public void method2() {}
  public void method4() {}
}
A的虚方法表B的虚方法表
index方法引用index方法引用
1A/method11A/method1
2A/method22B/method2
3A/method33A/method3
4B/method4

单继承子类的虚方法表会保留父类虚方法表的顺序,并覆盖、新增方法。调用 method2 时只需要按照虚方法表的位置进行调用。

JVM 提供了 itable 结构来支持多接口实现,itable由偏移量表和方法表组成。虚拟机可以在偏移量表中查找方法某个接口的方法表和方法位置,再去方法表中查找具体实现

以下示例中,由于多接口实现,无法通过固定的索引位置取得对应的办法,只能搜索 itable 获取对应方法

interface A {
  void method1();
  void method2();
}

interface B {
  void method3();
}

class D implements A, B {
  public void method1() {}
  public void method2() {}
  public void method3() {}
}

class E implements B {
  public void method3() {}
}
D类的 itableE类的 itable
index方法引用index方法引用
1method11method3
2method2
3method3

invokedynamic

MethodHandle

方法句柄,是 java.lang.invoke 包中的一个类,它使得 Java 可以把函数当作参数进行传递。MethodHandle 类似于反射中的 Method 类。

public class Foo {
    public void print(String s){
        System.out.println("Hello, " + s);
    }
    public static void main(String[] args) throws Throwable {
        Foo foo = new Foo();

        /**
         * 创建 MethodType 用来表示方法签名,每个 MethodHandle 都对应一个 MethodType 实例
         * 用来指定方法的返回值和参数类型
         */
        MethodType methodType = MethodType.methodType(void.class, String.class);

        /**
         * 调用 MethodHandles.lookup 静态方法返回 MethodHandles.Lookup 对象
         * 对象代表查找的上下文,根据方法的不同类型通过 findStatic, findSpecial, findVirtual
         * 查找方法签名为 MethodType 的方法句柄
         */
        MethodHandle methodHandle = MethodHandles.lookup().findVirtual(Foo.class, "print", methodType);

        // 拿到方法句柄后即可调用具体方法,使用 invoke 或者 ivokeExact 调用
        methodHandle.invokeExact(foo, "world");
    }
}

invokedynamic 把如何查找目标方法的决定权从虚拟机下放到具体的用户代码中。

  1. 首次执行 invokedynamic 会调用引导方法(Bootstrap Method)
  2. 引导方法返回一个 CallSite 对象,CallSite 内部根据方法签名进行目标方法查找,getTarget 返回方法句柄
  3. CallSite 没有变化则 MethodHandle 可以一直被调用,变化则需要重新查找
invokedynamic

相关技术

Lambda 表达式(书中描述有点流程化,需要阅读源码深入)

匿名内部类通过在编译期间生成新的 class 文件实现。(也就是,内部类也创建了对应的.class文件,并自动放入调用处)

public static void main(String[] args) {
    // 内部类
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello, inner class.");
        }
    };
    r1.run();
    
    // Lambda表达式
    Runnable r2 = ()->{
        System.out.println("Hello, lambda.");
    };
    r2.run();
}

但使用 Lambda 表达式并编译代码后发现并没有生成匿名内部类,也不存在多余的 .class 文件。

即,Lambda 表达式通过 invokedynamic 方式获取 BootstrapMethods 对应区域的字节码,调用 LambdaMetafactory.metafactory(),此函数返回 java.lang.invoke.CallSite 对象。获取到 CallSite 对象后就可以使用 getTarget 获取到目标方法句柄。

核心是 metafactory(),其通过 InnerClassLambdaMetafactory 类生成新的内部类(使用 ASM),新内部类实现了 Runnable 接口,在 run 方法中调用了 Lambda 所在类的静态方法(Lambda表达式中的内容被编译成静态方法)。

  • Lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时编译器生成一个对应的引导方法(Bootstrap Method)
  • 第一次执行 invokedynamic 指令会调用对应的引导方法,该引导方法调用 LambdaMetafactory.metafactory() 动态生成内部类
  • 引导方法会返回一个动态调用 CallSite 对象,这个对象会最终调用实现了 Runnable 接口的内部类
  • Lambda 表达式中的内容会被编译成静态方法,动态生成的内部类会直接调用该静态方法
  • 真正执行 lambda 调用的还是 invokeinterface 指令

泛型与字节码

public class Pair<T> {
  public T first;
  public T second;

  public Pair(T first, T second) {
    this.first = first;
    this.second = second;
  }
}

public void foo(Pair<String> pair) {
  String left = pair.left;
}

// 字节码
0: aload_1                  //pair入栈
1: getfield #2              //把 left 值加载到栈上,left的字段类型为Object
4: checkcast #4             //检查对象是否匹配给定类型,判断 left 是否为 String
7: astore_2                 //将栈顶 left 存回局部变量表
8: return
// 泛型擦除
public void print(List<String> list) {}
public void print(List<Integer> list) {}

以上代码会报错,因为编译后两个方法编译后字节码完全相同。

泛型擦除,指泛型使用时加上类型参数,编译时再抹掉的过程。由泛型附加的类型信息对 JVM 来说是不可见的。

  • 泛型类没有独有的 Class 类对象,如 List<String>.class ,只有 List.class
  • 不能用 primary 类型实例化类型参数,因为 <Integer> 会擦除变成 <Object>,<int> 却不行
  • 泛型异常也无法被捕获,捕获的异常在编译器无法确定旧无法生成对应的异常表
  • 不能声明泛型数组

synchronized 实现

synchronized 用于定义临界区,临界区是一次只能被一个线程执行的代码片段。

JVM 会检查方法 ACC_SYNCHRONIZED 标记位是否被设置为 1。如果有,执行线程会先尝试获取锁。

对于实例方法,JVM会把当前对象 this 作为隐式的监视器。

对于类方法,JVM会把当前类的类对象作为隐式的监视器。

同步方法完成后,无论是否正常返回都会释放锁。

public synchronized void increase() {
  ++count;
}

// 字节码
0: aload_0              // 将 this 对象引用入栈
1: dup                  // 复制栈顶元素
2: astore_1             // 存储到局部变量表
3: monitorenter         // 尝试获取栈顶 this 对象的监视器锁,成功则往下,失败则等待
4: aload_0              // 4~11 执行 ++count
5: dup
6: getfield #2          // Field count:I
9: iconst_1
10: iadd
11: putfield #2         // Field count:I
14: aload_1             // 将 this 对象引用入栈
15: monitorexit         // 调用 monitorexit 释放锁
16: goto  24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
  from      to      target      type
  4         16      19          any
  19        22      19          any

编译器为了保证无论同步代码块中的代码以何种方式结束,代码中每次调用的 monitorenter 必须有对应的 monitorexit 执行,因此编译器会自动生成一个异常处理器,保证如论如何都能正常释放锁。等价于:

public void __foo() throws Throwable {
  monitorenter(lock);
  try {
    bar();
  } finally {
    monitorexit(lock);
  }
}

反射相关

反射中设置有阈值 sun.reflect.inflationThreshold ,默认为 15。当反射方法调用超过 15 次时,会用 ASM 生成新类。

小于 15 次使用 Native 的方式来调用,不需要类的生成、校验和加载,虽然 Native 的方式比动态生成类调用要慢很多,但是使用字节码生成类第一次的开销很大,在复用程度较低情况下使用字节码生成类整体效率更低,因此设置阈值,超出则表示为高频调用,选择生成类来减少后续调用的开销。