java-方法调用
解析调用
解析调用是指对那些“编译器可知,运行期不变”的方法的调用,这些方法在编译期就可以确定,并且在解析阶段转换为直接引用,之后就不会改变了。可以进行解析调用的方法有:构造方法、私有方法、final方法、父类方法,这些方法在运行期都是唯一的,不会改变。
1 public class TempClazz { 2 public static void main(String[] args) { 3 TempClazz tempClazz = new TempClazz(); 4 System.out.println("main"); 5 tempClazz.fun(); 6 } 7 public void fun() { 8 A.fun(); 9 } 10 11 private static class A { 12 static { 13 System.out.println("this is A"); 14 } 15 public static void fun() { 16 System.out.println("this is A fun"); 17 } 18 } 19 }
-XX:+TraceClassLoading输出
[Loaded com.zyong.TempClazz from file:......] main [Loaded com.zyong.TempClazz$A from file:......] this is A this is A fun
上面测试中,A.fun()的解析并不发生在TempClazz加载阶段,而发生在A.fun()的调用阶段,解析后,A的常量池中对A.fun()的符号引用就会被转换为直接引用,在之后的运行中就可以直接调用了。
分派调用
java是一门单分派语言,分派是一门语言将方法调用连接到方法定义的一种方式。通常由接收者(方法调用者)、方法名(包含类名,比如java.lang.Object.hashCode)、实参等因素就可以确定分派,单分派就是只有一个因素决定的分派,多分派就是有多个因素决定的分派。
分派严格来说是动态的,目的就是在运行过程中找到方法定义,并没有静态分派一说。之所以是单分派,是由于java方法的调用只看调用者的实际类型,而不看实参的实际类型(只关注实参的静态类型,这在编译期就已确定)。如果既关注调用者的实际类型,又关注实参的实际类型就是多份派了。
java的多态就是通过分派,在运行时从多个方法版本中找到匹配的那一个。
解析调用和分派调用是不可能同时存在的,分派需要运行时确定方法版本(invokevirtual首先寻找实际类型,然后在实际类型中找对应的方法签名,最终返回直接引用),而解析在解析之后就不会再变了(解析会直接改变常量池中的符号引用为直接引用,而不像动态解析那样需要运行时去寻找直接引用)。
java对象在内存中的样子
如下图就是java对象中字段的布局
子类新增字段也没什么区别
对于方法的存储,如果每个对象都存储一份方法代码太低效了,方法逻辑是可以共享的,在每个对象中都会有一个Vtable(虚方法表)的引用,这个引用指向Vtable,Vtable中存储了各个方法的实际入口地址(如何在表中查找呢?)。虚表是分派调用才需要的,解析调用在解析阶段将符号引用替换为直接引用,之后的访问都会直接使用直接引用。
基于栈的解释器执行过程
1 public int calc() { 2 int a = 100; 3 int b = 200; 4 int c = 300; 5 return (a+b)*c; 6 }
解释器中,方法的执行涉及jvm指令、程序计数器、操作栈、局部变量表等。
如下图,程序计数器现在是0,表示执行偏移为0的jvm指令(下一条指令istore_1的偏移为2,可以理解为bipush 100是两条指令,bipush一条,100一条),bipush是将单字节整型常量压入操作栈中,istore_1是将栈顶的整型值出栈并存放在第1个局部变量中(第0个存this),sipush是压入4字节整型,iload_1是将局部变量表中的第1个变量复制到栈顶,iadd是将栈中头两个栈顶元素出栈并做整型加法再将结果入栈,imul是做整型乘法,ireturn是将栈顶的整型值返回。
参考:http://www.studytonight.com/java/dynamic-method-dispatch.php
https://www.programcreek.com/2011/11/what-do-java-objects-look-like-in-memory/