方法调用阶段是确定被调用方法,不涉及方法内部的具体运行过程,分为解析和分派,所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用
调用方法的字节码指令有:
前4条调用指令,分派逻辑都固化在JVM内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法决定
在类加载的解析阶段,非虚方法的符号引用转化为直接引用,称为解析(Resolution),非虚方法包括
虚方法的符号引用转化为直接引用延迟到运行时去完成,称为分派(Dispatch),其可能是静态或动态的
按照分派依据的宗量数可分为单分派和多分派,两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派,发生在编译阶段,典型应用为重载
对于如下程序
public class Test {
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(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();
test.sayHello(man);
test.sayHello(woman);
}
}
打印
hello,guy!
hello,guy!
Human称为变量的静态类型,Man则称为实际类型,静态类型在编译时可知,而实际类型在运行时才能确定
编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的,故上面选择选择了sayHello(Human)
但很多情况下,当重载版本不唯一时,只能选择一个相对更合适的方法
public class Test {
public static void sayHello(Object arg) {
System.out.println("hello Object");
}
public static void sayHello(int arg) {
System.out.println("int");
}
public static void sayHello(long arg) {
System.out.println("long");
}
public static void sayHello(Character arg) {
System.out.println("Character");
}
public static void sayHello(char arg) {
System.out.println("char");
}
public static void sayHello(char... arg) {
System.out.println("char ...");
}
public static void sayHello(Serializable arg) {
System.out.println("Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
对如上程序,打印char
对于如下程序
public class Test {
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();
}
}
打印
man say hello
woman say hello
woman say hello
使用javap转为字节码,0-15行是创建man和woman并存到局部变量表
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #16; //class Test$Man
3: dup
4: invokespecial #18; //Method Test$Man."":()V
7: astore_1
8: new #19; //class Test$Woman
11: dup
12: invokespecial #21; //Method Test$Woman."":()V
15: astore_2
16: aload_1
17: invokevirtual #22; //Method Test$Human.sayHello:()V
20: aload_2
21: invokevirtual #22; //Method Test$Human.sayHello:()V
24: new #19; //class Test$Woman
27: dup
28: invokespecial #21; //Method Test$Woman."":()V
31: astore_1
32: aload_1
33: invokevirtual #22; //Method Test$Human.sayHello:()V
36: return
16-17、20-21分别是man和woman调用sayHello对应的invokevirtual指令,指向常量仍为Human.sayHello,但最终执行的方法不同
invokevirtual指令的运行时解析过程如下:
字段没有多态,对于如下程序
public class Test {
static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Father, i have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Son, i have $" + money);
}
}
public static void main(String[] args) {
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}
}
打印输出
I am Son, i have $0
I am Son, i have $4
This gay has $2
动态分派选择方法时,需运行时在接收者类型的方法元数据中搜索合适的目标方法,为避免反复搜索,为类型在方法区中建立一个虚方法表(Virtual Method Table,vtable)
上图为上面程序的虚方法表,其存放各个方法的实际入口地址
相同签名的方法在父类、子类的虚方法表中都有相同索引,在类型变换时只需变更方法表即可找到对应方法
虚方法表一般在类加载的连接阶段进行初始化
方法的接收者(调用者)与方法的参数统称为方法的宗量
public class Test {
static class QQ {
}
static class _360 {
}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
对于方法hardChoice()
由上可知Java是一门静态多分派,动态单分派的语言
动态类型语言,其关键特征是其类型检查过程在运行时而不是编译时进行,常见的有JavaScript、Python,如下代码
obj.println("hello world");
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
而对于JavaScript,无论obj是何种类型,继承关系如何,只要有相同签名的println(String)方法即可调用成功
即动态类型语言变量无类型而变量值才有类型
JDK7之前要实现动态类型语言,需在编译时留个占位符类型,运行时动态生成字节码实现具体类型替换,但会导致
在此背景下,JDK7推出invokedynamic和java.lang.invoke用于支持动态类型语言
invoke通过方法句柄动态确认目标方法,如实现一个带谓词(指由外部传入用于排序时比较大小的动作)的排序函数
void sort(int list[], const int size, int (*compare)(int, int))
void sort(List list, Comparator c)
如下利用方法句柄模拟invokevirtual指令的执行过程
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class Test {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getPrintlnMH(obj).invokeExact("hello");
}
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
MethodType mt = MethodType.methodType(void.class, String.class);
return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
}
}
返回的MethodHandle对象可视为对调用方法的引用,由此Java就可实现类似函数指针的功能
void sort(List list, MethodHandle compare)
利用反射也能达到同样效果,但
invokedynamic指令与MethodHandle的作用是一样的
invokedynamic指令的位置称为动态调用点,参数为CONSTANT_InvokeDynamic_info常量,其包含
invokedynamic不能通过javap生成,如下简单举例
invokedynamic #123, 0
上面为invokedynamic指令,第一个参数为常量#123,第二个参数为0用于给实际方法占位
#121 = NameAndType #33:#30
#123 = InvokeDynamic #0:#121
常量#123是一个CONSTANT_InvokeDynamic_info,#0表示引导方法取Bootstrap Methods属性表的第0项,#121为方法类型和名称
对于如下继承结构,Son可以通过super访问Father的方法
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
}
}
但如果要访问GrandFather呢?JDK7之前很难处理,但JDK7后通过MethodHandles实现
public class Test {
public static void main(String[] args) {
new Son().thinking();
}
}
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
mh.invoke(this);
} catch (Throwable e) {
}
}
}