笔记参考书籍《深入理解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 指令。

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 | 方法引用 | |
1 | A/method1 | 1 | A/method1 | |
2 | A/method2 | 2 | B/method2 | |
3 | A/method3 | 3 | A/method3 | |
4 | B/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类的 itable | E类的 itable | |||
index | 方法引用 | index | 方法引用 | |
1 | method1 | 1 | method3 | |
2 | method2 | |||
3 | method3 |
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 把如何查找目标方法的决定权从虚拟机下放到具体的用户代码中。
- 首次执行 invokedynamic 会调用引导方法(Bootstrap Method)
- 引导方法返回一个 CallSite 对象,CallSite 内部根据方法签名进行目标方法查找,getTarget 返回方法句柄
- CallSite 没有变化则 MethodHandle 可以一直被调用,变化则需要重新查找

相关技术
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 的方式比动态生成类调用要慢很多,但是使用字节码生成类第一次的开销很大,在复用程度较低情况下使用字节码生成类整体效率更低,因此设置阈值,超出则表示为高频调用,选择生成类来减少后续调用的开销。