本文最后更新于 2025年8月4日 下午
调用
Java中设置了不同的指令来完成不同的调用过程 :
- invokestatic 用于调用静态方法
- invokespecial 用于调用实例构造器
<init>方法,私有方法和父类中的方法
- invokevirtual 用于调用所有的虚方法
- invokeinterface 用于调用接口方法,会在运行时再确定一个实现接口的对象
- invokedynamic 先在运行时动态解析处调用点限定符所引用的方法,再执行该方法
解析
对于静态方法,私有方法,实例构造器,父类方法,被final修饰的方法都会在类加载时将符号引用转为直接引用。
静态方法
对于静态方法,其调用版本在编译后就是确定的。相应的 静态方法和私有方法在类加载阶段进行解析
分派
静态分派与重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class StaticDispatch{ static abstract class Human{
} static class Man extends Human{
} static class Woman extends Human{
} public void sayHello(Human guy){ System.out.println("hello guy"); } public void sayHello(Woman guy){ System.out.println("hello lady"); } public void sayHello(Man guy){ System.out.println("hello gentleman"); } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.sayHello(man); sr.sayHello(woman); } }
|
静态分派是通过编译时的方法绑定确定了具体使用的方法。
即在编译时,通过引用声明,确定了变量的类型,再根据其类型,由编译器选择帮绑定对应的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Overload{ public static void sayHello(Object arg){ System.out.println("hello Object"); } public static void sayHello(int arg){ System.out.println("hello int"); } public static void sayHello(long arg){ System.out.println("hello long"); } public static void sayHello(Character arg){ System.out.println("hello char"); } public static void sayHello(char arg){ System.out.println("hello char ..."); } public static void sayHello(Serializable arg){ System.out.println("hello Serializable"); } public static void main(String[] args){ sayHello('a'); } }
|
优先级
精确匹配 → 自动类型提升 → 自动装箱/拆箱 → 父类型匹配 → 可变参数匹配
完整的调用过程分析
对于 sayHello('a') 的调用:
- 第一次调用 :精确匹配到
sayHello(char arg)
- 屏蔽 char 参数方法 :自动类型提升 char → int
- 匹配到
sayHello(int arg)
- 输出:
"hello int"
- 屏蔽 int 参数方法 :自动类型提升 int → long
- 匹配到
sayHello(long arg)
- 输出:
"hello long"
- 屏蔽 long 参数方法 :自动装箱 char → Character
- 匹配到
sayHello(Character arg)
- 输出:
"hello char"
- 屏蔽 Character 参数方法 :父类型匹配,Character → Serializable
- 匹配到
sayHello(Serializable arg)
- 输出:
"hello Serializable"
- (因为Character实现了Serializable接口)
- 屏蔽 Serializable 参数方法 :最终父类型匹配
- 匹配到
sayHello(Object arg)
- 输出:
"hello Object"
动态分派
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class DynamicDispatch{ static abstract class Human { protected abstract void sayHello(); }
static class Man extends Human{ @Override protected void sayHello(){ System.out.println("man say hello"); } } static class Woman extends Human{ @Override protected void sayHello(){ System.out.println("woman say hello"); } } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
|
对应字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static void main(java.lang.String[]); Code: Stack = 2 , Locals = 3 , Args_size = 1 0: new 3: dup 4: invokespecial 7: astore_1 8: new 11: dup 12: invokespecial 15: astore_2 16: aload_1 17: invokevirtual 20: aload_2 21: invokevirtual 24: new 27: dup 28: invokespecial 31: astore_1 32: aload_1 33: invokevirtual 36: return
|
动态分派过程
- 找到操作栈数栈顶的第一个元素所指向的对象的实例类型,记作C
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessErrorc异常
- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
单分派与多分派
单分派与多分派的区别是,在查询需要执行的方法是 是由某个量来决定需要执行的方法(单分派)还是由多个量来决定需要执行的方法(多分派)
单分派:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| javaclass Printer { public void print(Object obj) { System.out.println("打印Object: " + obj); } public void print(String str) { System.out.println("打印String: " + str); } }
Object obj = "Hello"; Printer printer = new Printer(); printer.print(obj);
|
多分派 :
1 2 3
| * Object obj = "Hello"; * printer.print(obj); *
|
实际查找过程
单分派查找 :
1 2
| 方法调用:receiver.method(arg1, arg2) 查找依据:receiver的运行时类型 + method名称 + 参数的编译时类型
|
多分派查找 :
1 2
| 方法调用:receiver.method(arg1, arg2) 查找依据:receiver的运行时类型 + method名称 + 所有参数的运行时类型
|
虚拟机动态分派实现
动态分派主要通过虚方法表来实现动态分派的支持。
虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有重写,那么子类中的虚方法表与父类中相同方法的地址入口是一致的。如果发生了重写,则将指向子类自己实现的方法入口。同时 父子类的虚方法表索引维持一致。这样在查找方法时,同一索引可以直接查询父类或者子类中的入口地址。
Java对动态语言特性的支持
Java中的动态语言实现是通过字节码指令invokedynamic来实现的
动态语言
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。
Invoke包
通过MethodHandle 允许定义一个对象接受一个方法名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class MethodHandleTest{ static class ClassA{ public void println(String a){ System.out.println(s); } } public static void main(String[] args) throws Throwable{ Object obj = System.currentTimeMills() %2 == 0 ? System.out : new ClassA(); getPrintlnMH(obj).invokeExact("icyfenix"); } private static MethodHandle getPrintlnMH(Object reveiver) throw Throwable{ MethodType mt = MethodType.methodType(void.class,String.class); return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver); } }
|
句柄与反射
- Reflection 与MethodHandle 机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。
- Reflection 的Method对象比MethodHandle 所包含的信息来得多,而MethodHandle中仅包含执行该方法的相关信息
Invoke指令
JVM中有5种invoke指令,分别用于不同类型的方法调用。它们在绑定时机、查找机制和性能上各有特点。
invokestatic - 静态方法调用
- 绑定时机 : 编译时静态绑定
- 需要实例 : 否
- 支持多态 : 否
- 性能 : 最高
使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class MathUtils { public static int add(int a, int b) { return a + b; } }
* int result = MathUtils.add(5, 3);`
字节码示例
iconst_5 iconst_3 invokestatic #2 istore_1
|
特点
- 直接根据类名和方法签名调用
- 无需运行时查找,性能最优
- 方法地址在编译时确定
invokespecial - 特殊方法调用
- 绑定时机 : 编译时静态绑定
- 调用对象 : 构造方法、私有方法、super方法
- 支持重写 : 否
- 性能 : 高
使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Animal { private void privateMethod() { } public Animal() { * super(); * privateMethod(); * } }
字节码示例
aload_0 invokespecial #1 aload_0 invokespecial #2
|
特点
- 用于不能被重写的方法调用
- 编译时确定目标,安全且高效
- 包括构造器、私有方法、父类方法调用
invokevirtual - 虚方法调用
- 绑定时机 : 运行时动态绑定
- 查找结构 : 虚方法表(vtable)
- 支持多态 : 是
- 性能 : 中等
使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class Animal { public void makeSound() { System.out.println("Animal sound"); } }
class Dog extends Animal { @Override public void makeSound() { System.out.println("Woof!"); } }
Animal animal = new Dog(); animal.makeSound(); *
字节码示例
aload_1 invokevirtual #3
查找机制
* class Animal { vtable[0] = toString() * vtable[1] = hashCode() * vtable[2] = makeSound() * }
class Dog extends Animal { vtable[0] = toString() * vtable[1] = hashCode() * vtable[2] = makeSound() * }
*
|
特点
- 通过vtable进行O(1)查找
- 平衡了多态性和性能
- 支持方法重写和动态分派
invokeinterface - 接口方法调用
基本特性
- 绑定时机 : 运行时动态绑定
- 查找结构 : 接口方法表(itable)
- 多接口支持 : 是
- 性能 : 较低
使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| interface Drawable { void draw(); }
class Circle implements Drawable { public void draw() { System.out.println("Drawing circle"); } }
Drawable drawable = new Circle(); drawable.draw(); *
字节码示例
aload_1 invokeinterface #4, 1
查找机制
java
* interface Drawable { void draw(); } interface Movable { void move(); }
class Circle implements Drawable, Movable { * itable_Drawable[0] = draw() itable_Movable[0] = move() }
*
|
特点
- 需要在多个接口表中查找
- 支持多接口实现
- 性能开销较大,但提供了高度的灵活性
invokedynamic - 动态方法调用
- 引入版本 : Java 7
- 绑定时机 : 首次调用时动态绑定
- 主要用途 : Lambda表达式、动态语言支持
- 性能 : 优化后较高
使用场景
1 2 3 4 5 6 7 8 9 10 11
| * List<String> list = Arrays.asList("a", "b", "c"); list.forEach(s -> System.out.println(s));
* list.forEach(System.out::println);
### 字节码示例
invokedynamic #2, 0
|
特点
- 通过BootstrapMethod在运行时决定调用目标
- 支持现代Java特性和动态语言
- 首次调用后会缓存结果,后续调用性能较高