1. 方法调用
方法调用不同于方法执行,调用阶段主要确定方法调用者的版本,方法调用的目标方法在Class文件都是常量池中的符号引用。而在方法表的code属性中有对应调用该方法的字节码指令。那么在解析阶段,会把一部分符号引用转化成直接引用,那么是哪一部分呢?
解析调用
能在解析阶段将方法的符号引用转化成直接引用的的方法,必须在方法运行前就确定一个可调用的版本,并且这个版本在运行阶段是不可改变的。就是方法“编译期可知,运行期不可变”,符合这个规则的方法有静态方法和私有方法两大类。前者与所属的类直接有关系,后者在外部不可以被访问。这两种方法都适合在解析调用,也就是把这些方法的符号引用转化成直接引用
在JVM中有5条调用方法的字节码指令
- invokestatic:调用静态方法
- invokespecial:调用实例构造器方法,私有方法和父类方法
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法,运行时确定一个实现此接口的对象
- invokedynamic:先在运行时动态解析出调用点限定符所引用的的方法,然后再执行此方法
只有用invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定调用版本,这些方法叫做非虚方法。
剩下三个字节码指令调用的方法叫做虚方法(invokevirtual指令调用的final修饰的方法除外)
解析调用是一个静态过程,编译期间就可以确定,解析阶段将符号引用转化为直接引用。
分派
分派调用可能是静态的也可能是动态的,是实现多态性的体现
静态分派
根据方法参数的静态类型确定调用方法的版本,在编译期间确定,典型的应用就是重载
package xidian.lili.Demo01;
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{
}
static class Woman extends Human{
}
public void sayHello(Human human){
System.out.println("human is saying hello");
}
public void sayHello(Man man){
System.out.println("man is saying hello");
}
public void sayHello(Woman woman){
System.out.println("woman is saying hello");
}
public static void main(String[] args) {
Human woman=new Woman();
Human man=new Man();
StaticDispatch sd=new StaticDispatch();
sd.sayHello(man);
sd.sayHello(woman);
}
}
在上述的程序示例中,有三个重载方法,根据传入的对象类型的不同而不同。
sd.sayHello(man);
sd.sayHello(woman);
方法调用的语句可以看出,方法调用的对象是sd,然后具体执行哪个方法取决与方法的参数
参数man创建语句:Human man=new Man();是一个父类引用指向子类的对象,对于变量man来说,Human是她的静态类型,Man是它的动态类型。在编译阶段可以确定的是变量的静态类型,也就是编译器根据静态类型选择了sayHello(Human)为调用目标,就把这个方法的符号引用写入了main方法的两条invokevirtual的指令参数中
静态分派的模糊结论
在确定调用的方法的对象的情况下,由参数决定的调用方法不唯一,编译器就会确定一个更加合适的方法来调用。
下面例子中有不同类型参数的重载方法
package xidian.lili.Demo01;
import java.io.Serializable;
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 object");
}
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');
}
}
当调用sayHello(‘a’),自然调用的char类型参数的方法,输出hello char
当注释掉char的重载方法,输出hello int,发生一次自动转型,'a’可以代表字符,也可以代表数字97,所以调用int类型的重载方法
再注释掉int的方法,输出hello long,发生两次类型转化,会按照char->int->long->float->double的顺序匹配。但不会向下转型调用方法,不安全。还找不到就会调用char的自动装箱类型,输出hello Character。继续注释掉Character的重载方法,输出"hello serializable",因为char的包装类Character实现了接口Serializable.继续注释掉,输出hello object。Object是所有类的父类,如果有多个父类,那将从下到上开始搜索,找到可以调用的重载方法。继续注释掉,才会输出长参数的结果hello char…
上述例子展示了在编译期间选择静态分派目标的过程,这就是java实现方法重载的本质
解析调用和静态分派是不同层次的筛选,确定目标方法的过程,静态调用是在编译期间将静态方法,构造方法等符号引用转化为直接引用,但是静态方法和构造方法也有重载方法,所以在编译期间也会对这些方法进行静态分派
动态分派
只有在运行期间根据对象的实际类型能确定调用方法的版本,典型的应用就是方法的重写
package xidian.lili.Demo01;
public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man is say hello");
}
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman is 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.sayHello();woman.sayHello();显然不是根据对象的静态类型来确定调用方法的版本。这次确定调用哪个方法取决与man和woman两个对象的类型。这两个方法的字节码指令都是invokevirtual,invokevirtual指令的运行时解析如下步骤:
(1)找到操作数栈栈顶的第一个元素所指向的对象的实际类型。(确定这个方法属于哪个类)
(2)如果在该类型中找到与常量描述符和简单名称相符的方法,就进行访问权限,通过则返回方法的直接引用,没有通过则抛出异常
(3)如果在类型中没有找到对应的方法,则按照继承关系从下往上依此查找方法
invokevirtual指令执行的时候先确定方法调用的对象的实际类型,所以会把两次方法调用的符号引用解析到不同的直接引用上,这个过程叫做动态分派
单分派与多分派
单与多取决于方法宗量的个数。方法的接受者和方法的参数称为方法的宗量。单分派就是根据一个宗量确定调用方法是哪个,多分派就是根据多于一个宗量对目标方法进行选择。
package xidian.lili.Demo01;
public class Dispatch {
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 s=new Son();
father.hardChoice(new _360());
father.hardChoice(new QQ());
s.hardChoice(new _360());
s.hardChoice(new QQ());
}
}
当编译期间,调用hardChoice方法,有两个标准,一是静态类型是Father还是Son,而是参数是QQ还是360,选择完的两条invokevirtual指令的参数为father.hardChoice(_360);father.hardChoice(QQ);s.hardChoice(_360);s.hardChoice(QQ);的符号引用。因为是根据两个宗量进行选择,所以静态分派属于多分派类型
而在程序的运行期间,执行编译期间生产的四条字节码指令,参数类型已经确定,虚拟机不会关心参数是QQ还是360,唯一还可以变化的是方法接受者的实际类型,根据invokevirtual指令的多态查找确定接受者的实际类型,只有一个宗量的考虑,所以动态分派属于单分派类型
Java动态分派的实现
在类的方法区建立一个虚方法表,表中存放着各个方法的实际入口地址。如果方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类的方法入口是一致的,都指向父类的实现入口
如果子类中重写了这个方法,子类方法表中的地址会替换为子类实现版本的入口地址
方法表一般在类加载的阶段进行初始化
2. 线程上下文类加载器
我们知道JVM虚拟机采用双亲委派模式来加载类,而且在类加载的整个过程中只有在加载阶段可以别程序员操作,加载器通过类的全限定名在class文件的二进制流中加载类,并创建类的唯一一个class对象,作为类的全局访问点。我们知道为了实现程序的动态性,我们可以自定义类加载器,通过重写findClas()方法来实现自定义类加载器,再通过重写loadClass()方法打破双亲委派机制,这是一次打破双亲委派,那么还有一种就是使用线程上下文类加载器来打破双亲委派机制。
典型应用就是我们知道java提供一些第三方服务接口(SPI),用的最多的就是jdbc接口,提供数据库服务,允许不同的第三方来实现,但是我们需要来java代码中加载第三方驱动,获得具体的实现代码