深入理解java虚拟机(10):方法调用
方法调用不等于方法执行,方法调用阶段的唯一任务就是确定被调用的方法的版本。class文件编译期间不包含传统程序的连接过程,因此方法不是实际内存运行的入口地址,这个特性给java带来了动态扩展的能力,也使java的方法调用过程变得更加复杂,需要在类加载期间甚至运行期间才能确定目标方法的引用。
1、解析
方法调用目标方法在class文件里面是对常量池中的一个符号引用,在类加载解析阶段会将其中的一部分符号引用转换成直接引用。转换的前提是在程序真正运行前就有一个可以确定的可用版本,并且这个方法的调用版本在运行期间不可变。换句话说就是调用目标程序代码写好、编译器编译期间就必须确定下来。这类方法调用被称为解析。
符合编译期间可知,运行期间不可变的方法只有静态方法和私有方法。前者与类型直接关联,后者外部不可被访问因此无法被覆盖重写,因此适合在类加载阶段进行解析,与之相对应的是java虚拟机中的5条直接码指令
invokestatic静态方法调用,invokespecial 调用实例构造器init方法,私有方法和类方法,invokevirtual调用虚方法,invokeinteface调用接口方法,运行时确定一个具体实现类的方法,invokedynamic先在运行动态解析出调用点限定符号所引用的方法,再执行该方法。前四条指令的分派逻辑在java虚拟机内部固化,invokedynamic指令的分派逻辑由用户设定的引导方法决定。
解析调用是一个静态的过程,在编译期间就完全确定,在类装载的解析阶段就将符号引用转换成了直接引用,不会延迟到运行期间再去完成。
2、分派
1)静态分派
package org.xiaofeiyang.classloader;
/**
* @author: yangchun
* @description:
* @date: Created in 2019-11-24 11:07
*/
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(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();
StaticDispatch staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
运行结果
Hello guy
Hello guy
上面Human类型称为变量的静态类型或者外观类型,静态类型编译期间可知,实际类型动态运行时才可知。上面代码编译器通过参数的静态类型而不是实际类型确定重载方法,所以选择
sayHello(Human guy)作为调用目标,并把这个方法的符号引用写到main()方的两条invokedynamic指令里面。依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派发生在编译期间。
package org.xiaofeiyang.classloader;
import java.io.Serializable;
/**
* @author: yangchun
* @description:
* @date: Created in 2019-11-24 11:26
*/
public class Overload {
public static void sayHello(Object arg){
System.out.println("Hello Objec");
}
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 Character");
}
public static void sayHello(char 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");
}
}
这个代码可以非常好的验证静态分派。
2)动态分派
invokevirtual运行时解析过程如下
1)找到操作数栈第一个元素所指向的对象的实际类型,记作C
2)如果在类型c中找到与常量中描述符和简单名称都相符合的方法就进行权限验证,验证通过则返回这个方法的直接引用
3)否则则按照第二步在c的父类中搜索和验证
动态分派主要是看接收对象的实际类型,实际虚拟机从性能的角度不会进行这么大范围的搜索,会建立一个虚方法表,
方法表一般在连接阶段开始初始化,在类将变量初始值后,虚拟机就会把该类的方法表也初始化完。除了方法表还会使用内联缓存和基于类型继承分析技术的守护内联,
3、动态语言类型的支持
invokedynamic是为java8 lambda表达式提供支持而新增的,编译期间进行类型检查的是静态语言,运行时类型检查的是动态语言。
如obj.println(),必须是obj的变量类型有println方法,而不是obj实际类型有println方法,但是动态语言没有类型只要obj实际有printn方法就可以。直白的就是动态语言没有类型。java之前的四条指令集invokevitrual
invokespecial,invokeinterface,invokestatic第一个参数都是constant_methodref_info或者constant_interfacemethodref_info,方法符号的引用如this在编译期间生成,但是动态语言在运行时才能知道它的具体类型,所以虚拟机上实现动态语言就不得不留占位符,运行时动态生成字节码实现具体类型到占位符类型的适配,会额外增加开销。invokedynamic把查找方法的决定权从虚拟机转移到具体的用户代码中。这条指令第一个参数不再是Constant_methodref_info而是Constant_InvokeDynamic_info常量,有三类信息引导方法,方法类型,名称。引导方法是固有值,java.lang.invoke.callsite对象,找到引导方法,并且执行返回一个callsite对象,最终调用要执行的目标方法。
package org.xiaofeiyang.classloader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import static java.lang.invoke.MethodHandles.lookup;
/**
* @author: yangchun
* @description:
* @date: Created in 2019-11-25 9:33
*/
public class MethodHandleTest {
static class ClassA{
public static void println(String s){
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable{
Object object = System.currentTimeMillis()%2==0?System.out:new ClassA();
getPrintlnMH(object).invokeExact();
}
private static MethodHandle getPrintlnMH(Object reviever) throws Throwable{
MethodType mt = MethodType.methodType(void.class,String.class);
return lookup().findVirtual(reviever.getClass(),"println",mt);
}
}
再来一个invokedynamic自己控制调用逻辑
package org.xiaofeiyang.classloader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import static java.lang.invoke.MethodHandles.lookup;
/**
* @author: yangchun
* @description:
* @date: Created in 2019-11-25 10:10
*/
public class invokeDynamicTest {
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 methodType = MethodType.methodType(void.class);
MethodHandle methodHandle = lookup().findSpecial(GrandFather.class,"thinking",methodType,getClass());
methodHandle.invoke(this);
}catch (Throwable e){
}
}
}
public static void main(String[] args){
(new invokeDynamicTest().new son()).thinking();
}
}