微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路
重载和重写
在java中,如果同一个类出现了多个名称相同,参数也相同的方法,是无法通过编译的。就是说,在同一个类中定义相同名字的方法,该方法的参数必须不同,这就叫做重载。重载方法在编译过程中就可以被识别。java编译器会根据所传入的参数的声明类型来选取重载方法。共有三个阶段
不考虑基本类型的自动装拆箱和可变长参数的情况下选;
在(1)没找到的话,允许装拆箱但不允许可变长参数的情况下选;
允许装拆箱和可变长参数的情况下选;
如果在一个阶段找到了多个适配的方法,java编译器就会选择一个最合适的方法。这个判断什么是合适的关键就是集成关系。也就是说,同样的参数,子类更优先。给个例子
public void test1(Object object){
System.out.println("我是Object");
}
public void test1(String object){
System.out.println("我是String");
}
public static void main(String[] args) {
test t = new test();
t.test1(null);
}
执行的话输出的就是“我是String”。这个调用在编译器选取方法的时候,在第一步就能找到2个合适的方法,但是由于String是Object的子类,所以java编译器会认为以String为参数的更合适
JVM的静态绑定和动态绑定
jvm识别方法在于类名,方法名和方法描述符。对于方法描述符,是由参数类型以及返回值类型构成的。jvm和java语言不同,jvm并不限制一个名字,参数相同但返回类型不同的方法在同一个类中。因为调用这些方法的字节码所附带的方法描述符包含了返回值类型,所以JVM能识别这些方法。在jvm中,如果子类定义了和父类中非私有,非静态的同名方法,只有这两个方法的参数和返回类型一直,jvm才会判定是重写。
在jvm中,静态绑定值的是在解析时便能够直接识别出目标方法;动态绑定指需要在运行过程中根据调用者的动态类型来确认目标方法。java字节码中于调用有关的指令共有5种
- invokestatic:调用静态方法
- invokespecial:调用私有实例方法、构造器、父类的实例方法,构造器 实现接口的默认方法
- invokevirtual:调用非私有实例方法
- invokeinterface:调用接口方法
- invokedynamic:调用动态方法
调用指令的符号引用
因在编译过程中,并不知道目标方法的具体地址,所以java编译器会暂用符号引用来表示目标方法。符号应用就包含目标方法所在的类或接口名,方法名和方法签名。
符号引用可分为接口符号应用和非接口符号引用。在java虚拟机---JVM加载类的过程简单介绍中说过,jvm会把符号引用解析成实际引用。对于非接口引用,会有如下步骤:
在目标类中查找符合名字及描述的方法
(1)没找到就在目标类的父类里继续查找,直到Object类还没找到的话,就再目标类直接或间接实现的接口里查找非私有,非静态的方法对于接口应用,步骤如下
- 在目标里查找符合名字及描述的方法
- 没找到就再Object类中的公有实例方法
- 还没找到就再目标接口的超类里搜索。
- 解析之后,符号应用就会被解析成实际引用。如果是静态绑定的方法就会解析成一个指向方法的指针。动态绑定的话,则是一个方法表的索引。
下面具体看看方法的调用
虚方法调用
java里所有的非私有实例方法调用指令是invokevirtual,接口方法调用的指令是invokeinterface。这两种指令都属于虚方法调用。在大多数情况下,jvm需要根据调用者的类型来确定调用目标的方法,这个过程就叫做动态绑定。当然,如果一个虚方法指向一个final修饰的方法,那么也可以静态绑定。
在jvm里,静态绑定的指令invokestatic和invokespecial。
方法表
方法表就是在类加载机制的链接部分中,构造出来与被加载类关联的数据结构。这个数据结构就是jvm实现动态绑定的关键。
方法表的本质上是一个数组,每个元素都指向当前类或超类中的非私有的实例方法。包含两个特性
- 子类方法表包含父类方法表中的所有方法
- 子类方法表的索引值和它重写的父类方法的索引值相同
在执行过程中,jvm获取调用者的实际类型,并在实际类型的虚方法表中根据索引获取目标方法。这个过程就叫做动态绑定。使用了方法表的动态绑定与静态绑定相比,也就只多出了几个内存解析操作而已:
- 访问栈上的调用者
- 读取调用者的动态类型
- 读取该类型的方法表
- 读取方法表中的某个索引对应的目标方法
相对于java栈的初始化来说,这几个步骤的开销可以忽略不计。这种方法其实仅存在于解释执行中。因为对于即时编译来说,有另外两种更好的手段 内联缓存和方法缓存。
内联缓存
内联缓存就是缓存调用者的动态类型和目标类型。在之后的执行过程中,碰到已经缓存的类型内联缓存就直接调用目标类型,没有碰到的话,就使用方法表去动态绑定。
对于内联缓存来讲,有单态内联缓存、多态内联缓存以及超多态内联缓存。单态内联缓存就是只缓存了一种动态类型和它所对应的目标方法。实现很简单,比较锁缓存的动态类型,如果命中,就直接调用目标方法。多态内联缓存:缓存了多个动态类型和目标方法。会挨个的将缓存的类型和调用的动态类型进行比较,命中就调用目标方法。通常来讲,JVM会把热点方法放在更前面以便调用。
当然,内联缓存不一定就能优化你的代码,提高执行性能。因为在内联缓存没有命中时,jvm会使用方法表进行动态绑定。这时,对于内联缓存中的数据,就有两种选择。
替换单态内联缓存中的数据。这种就好比数据缓存,在理想的情况下,替换后的一段时间,调用者的方法类型一致,就能够有效的利用内联缓存。在最坏的情况下,使用两种不同的方法轮流调用,那就只能带来额外的写开销而没有一点性能提升
劣化为直接访问方法表。这样就牺牲了优化的可能,但能减少额外的写开销
总结
在java中,对于方法来讲,存在重载和重写的概念。重载指的是方法名相同而参数不同,重写则是方法名和参数都相同。
但jvm则有稍微的不同,对于方法来说,除了方法名和参数,还要加上返回类型。静态绑定指的是能在解析阶段就能识别目标方法,而动态绑定则是在运行过程中根据调用者的动态类型来识别方法的情况。
在jvm中,方法调用指令共有5中,其中invokestatic和invokespecial是静态绑定的指令。invokevirtual和invokeinterface是虚方法调用。但如果指向的是final修饰的方法,那么就是静态绑定,否则就是动态绑定。
jvm的动态绑定是通过方法表来实现的。jvm的即时编译会使用内联缓存来加速动态绑定。当碰到调用者时,如果动态类型与缓存的类型匹配,就直接调用缓存的目标方法,不然就会劣化为超多态内联缓存,以后的执行过程都是使用方法表去绑定