6.上下文类加载器

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的指令参数中
image

静态分派的模糊结论

在确定调用的方法的对象的情况下,由参数决定的调用方法不唯一,编译器就会确定一个更加合适的方法来调用。

下面例子中有不同类型参数的重载方法

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代码中加载第三方驱动,获得具体的实现代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值