title: 类加载机制(五):自定义类加载器与深入双亲委托机制
date: 2019-03-17 08:24:05
categories:
- Java虚拟机
tags: - 类加载机制
- 自定义类加载器
引言
我们知道类加载器共分为两大类型,Java
虚拟机自带的类加载器和自定义类加载器。Java
虚拟机自带的类加载器分别加载了不同路径下的class
文件,而有时我们需要加载一些特殊的class
文件,如这个class
文件是被加密的,我们就需要自己定义类加载器去解密加载它,又比如我们需要从网络或者直接从数据库中读取class
文件,我们也需要自己定义类加载。
上文(类加载机制(四):解析ClassLoader)我们介绍分析了ClassLoader
类,知道这个类是一个抽象类,除了Java
虚拟机内建的启动类加载器以为,所有的类加载器都继承于它,并且要重载它的一个方法findClass
去搜寻指定名字的class
文件,并且如果在一个类中,又有其他类的引用,也是先通过调用类的类加载器先尝试去加载。在此篇文章,我们自定义一个类加载器去加载本地文件系统中的class
文件来深入剖析双亲委托机制。
自定义类加载器
首先来看看代码。
public class MyClassLoader extends ClassLoader{
//定义一个className,表示自定义类加载器的名字
private String className;
//定义一个path,表示class文件所在目录
private String path;
public void setPath(String path) {
this.path = path;
}
//同其父类ClassLoader一样,有两个构造方法
public MyClassLoader(ClassLoader parent, String className) {
super(parent);
this.className = className;
}
public MyClassLoader(String className) {
super();
this.className = className;
}
//重载的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//测试自定义类加载器是否执行成功
System.out.println("自定义class loader name: " + this.className);
//调用MyLoadClass获取字节数组
byte[] bytes = this.MyLoadClass(name);
//调用
return defineClass(null,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private byte[] MyLoadClass(String className) throws IOException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream bis = null;
//将传入的类的二进制名转换为类的全限定名(包名+类名)
String replace = className.replace(".", File.separator);
try {
//将这个class文件转换成字节数组
is = new FileInputStream(this.path + replace + ".class");
bis = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())){
bis.write(ch);
}
data = bis.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
is.close();
bis.close();
}
return data;
}
}
如上代码所示,这个简易的自定义类加载器同样有两个构造方法,没有父类加载器传入的构造方法会调用
ClassLoader
的无参的构造方法,将系统类加载器设置为这个自定义类加载器的父类加载器;有父类类加载器传入的构造函数,也会调用ClassLoader
的构造方法,只不过调用的是有参的构造方法,将传入的这个类加载器设置为这个自定义类加载器的父类加载器。
测试
具体的实现,在注释中,就不过多阐述了。我们在findClass
中写了句测试语句System.out.println("自定义class loader name: " + this.className);
,来测试自定义类加载器是否执行成功。到这我们就可以在启动类中进行测试了。
public static void executeLoad(MyClassLoader loader,String className) throws Exception{
loader.setPath(***********);
Class<?> loadClass = loader.loadClass(className);
//打印出Class对象的hash码
System.out.println(className + "的class对象的hashCode:" + loadClass.hashCode());
//创建一个示例
Object o = loadClass.newInstance();
System.out.println("--------------");
}
写了一个执行方法,减少代码,并且在指定的
path
中放入MyTest1
的class
文件:
- 传入自定义类加载器的实例与要加载的类的二进制名字。
- 在方法体里面指定好要加载的
class
文件目录。- 调用父类的
loadClass
方法进行加载(ClassLoader
具体怎么加载,见类加载机制(四):解析ClassLoader)。
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"classLoader.MyTest1");
}
输出结果:
classLoader.MyTest1的class对象的hashCode:1163157884
--------------
竟然只输出了
MyTest1
的class对象的hashCode
,意思是我们的自定义类加载未执行(System.out.println("自定义class loader name: " + this.className);
)(⊙o⊙),怎么回事?
结果分析
我们知道关于类的加载也就是class
文件的搜索与加载过程是由类加载器完成的,而类加载器又是遵循双亲委托机制的,关于这个机制就不多说了,见以前的文章。
在MyClassLoader
中我们首先调用ClassLoader
的loadClass
方法,在loadClass
中,最终会调用我们重载的这个findClass
方法,但现在我们重载的findClass
并没有被调用,说明有其他的findClass
调用了。那我们在executeLoad
中打印下加载的这个class
对象的类加载器。
System.out.println("我就是它加载的:" + loadClass.getClassLoader());
输出结果:
我就是它加载的:sun.misc.Launcher$AppClassLoader@18b4aac2
classLoader.MyTest1的class对象的hashCode:1163157884
--------------
结果显示
MyTest1
是由系统类加载器加载的。
现在水落石出了,原来我们想要加载的MyTest1
被系统类加载器给加载了,那为什么呢,其实联想下双亲委托机制就明白了。MyClassLoader
收到要加载某个类的请求,就往其父类加载器(系统类加载器)传递,然后,一层层传递,导启动类加载器后,又往下传回来,传到系统类加载器后,系统类加载器发现自己能加载这个类,然后就截胡了,MyTest1.class
就被系统类加载器加载到内存中去了。
我们知道,系统类加载器是从classPath
或者java.class.path
系统属性中去加载class
文件和jar
包的,那我们把classPath
中的MyTest1.class
给删除掉,结果又会怎么样呢?
输出结果:
自定义class loader name: loader1
我就是它加载的:classLoader.MyClassLoader@4554617c
classLoader.MyTest1的class对象的hashCode:356573597
--------------
输出结果显示:我们的
MyClassLoader
起作用啦,注意这里hashCode
不一样哦(⊙x⊙;)。
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"classLoader.MyTest1");
MyClassLoader loader2 = new MyClassLoader("loader2");
MyClassLoader.executeLoad(loader2,"classLoader.MyTest1");
//loader2是loader3的类加载器
MyClassLoader loader3 = new MyClassLoader(loader2,"loader3");
MyClassLoader.executeLoad(loader3,"classLoader.MyTest1");
}
再创建两个
MyClassLoader
的实例。loader2–>loader3
输出结果:
自定义class loader name: loader1
我就是它加载的:classLoader.MyClassLoader@4554617c
classLoader.MyTest1的class对象的hashCode:356573597
--------------
自定义class loader name: loader2
我就是它加载的:classLoader.MyClassLoader@677327b6
classLoader.MyTest1的class对象的hashCode:2133927002
--------------
我就是它加载的:classLoader.MyClassLoader@677327b6
classLoader.MyTest1的class对象的hashCode:2133927002
--------------
输出结果显示:
loader2
加载获得的class
对象和loader3
加载获得的class
是一样的。
这个结果其实ClassLoader
类中的loadClass
很清楚:
- 类只会被加载一次(
findLoadedClass(String)
),返回的class
对象都一样。若没有class
文件,则会调用当前加载器的findClass
方法去查找class
文件。 - 双亲委托机制是包含关系,实例化
loader3
时可以让loader2
作为自己的父加载器,创建loader3
去加载MyTest1
时,因为loader2
已经加载过了(findLoadedClass(String)
),所以使用loader3
加载时,loader3
直接返回了已经加载过的MyTest1
的class
对象。
深入双亲委托机制
我们通过一些示例代码来进行分析。
示例代码
NO.1
public class MyCat {
public MyCat(){
public MyCat(){
//打印出MyCat的类加载器器
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
}
}
}
--------------------------------------------------------
public class MySample {
public MySample() {
//打印出MySample的类加载器器
System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
System.out.println("--------------");
//在MySample的构造方法中创建一个MyCat的实例
new MyCat();
}
}
--------------------------------------------------------
public class MyTest13 {
public static void main(String[] args) throws Exception {
//加载MySample类
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader.executeLoad(loader1,"refenLoad.MySample");
}
}
输出结果:
refenLoad.MySample的class对象的hashCode:1956725890
MySample is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
具体过程如下:
- 调用
MyClassLoader
加载MySample
类。classPath
中有MySample
类的class
文件,系统类加载器将其加载到内存中。- 然后因为在
executeLoad
方法中创建了对象实例,MySample
被首次主动使用,即进行初始化,调用构造函数完成初始化。- 在
MySample
的构造函数中new MyCat()
,即对MyCat
的首次主动使用,经历加载连接初始化。
接着,复制一份MySample
的class
文件到我们设定的path
中,删除到classPath
中的那份,结果怎么样呢。
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
为什么,两个类的类加载器又不一样呢?
- 因为
classPath
中没有MySample
的class
文件,所以经过双亲委托机制,最终是通过MyClassLoader
来加载我们自己的MySample
文件。 - 创建
MySample
实例时,进行MySample
的初始化,执行MySample
的构造方法。 MySample
的构造方法里创建MyCat
实例,使用加载MySample
的类加载器来加载MyCat
。MyClassLoader
加载器委托系统加载器来加载MyCat.class
,加载完成。
再接着,复制一份MyCat
的class
文件到我们设定的path
中,删除到classPath
中的那份,结果又怎么样呢。
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
自定义class loader name: loader1
MyCat is loaded by:classLoader.MyClassLoader@74a14482
--------------
它们的类加载器又都是
MyClassLoader
了。
- 因为
classPath
中没有MySample
的class
文件,所以经过双亲委托机制,最终是通过MyClassLoader
来加载我们自己的MySample
文件。 - 创建
MySample
实例时,进行MySample
的初始化,执行MySample
的构造方法。 MySample
的构造方法里创建MyCat
实例,使用加载MySample
的类加载器MyClassLoader
来加载MyCat
,加载成功。
如果只删除MyCat.class
又会怎么样呢?
系统加载器加载MySmple.class
,加载MyCat
时,同样使用系统加载器来加载MyCat
,但classPath
中没有MyCat.class
文件,最后就会抛出java.lang.NoClassDefFoundError
异常。
再再接着,reBuild
项目,删除掉MySample
的class
文件,在MyCat
的构造方法里打印MySample
的class`。
public class MyCat {
public MyCat(){
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
System.out.println("from MyCat:" + MySample.class);
}
}
输出结果:
自定义class loader name: loader1
refenLoad.MySample的class对象的hashCode:1735600054
MySample is loaded by:classLoader.MyClassLoader@74a14482
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
![](http://res.cloudinary.com/cafebabe/8WB0J_LXX_22D_E_3U_G_E.png)
我们想在
MyCat
中调用MySample
,竟然报错了,找不到MySample
类,这里涉及到类的命名空间问题。
最后,只删除classPath
中的MySample
的class
文件,在MySample
的构造方法中打印MyCat
的class
。
public class MyCat {
public MyCat(){
System.out.println("MyCat is loaded by:" + this.getClass().getClassLoader());
}
}
-----------------------------------------------
public class MySample {
public MySample() {
System.out.println("MySample is loaded by:" + this.getClass().getClassLoader());
System.out.println("from MyCat:" + MyCat.class);
System.out.println("--------------");
new MyCat();
}
}
输出结果:
refenLoad.MySample的class对象的hashCode:1956725890
MySample is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
from MyCat:class refenLoad.MyCat
--------------
MyCat is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
--------------
可以看到,这里就打印成功,也就是说,在
MySample
中调用MyCat
成功,这里同样也是命名空间的问题。
命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
命名空间之间的关系
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此由只加载器加载的类能看见父加载器加载的类。例如系统类加载器可以看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类互不可见。
了解了命名空间后,就明白前面代码的输出结果了。
(1)删除掉MySample
的class
文件,在MyCat
的构造方法里打印MySample
的class
。MySample
由MyClassLoader
加载,MyCat
由AppClassLoader
加载,父加载器加载的类是看不到子加载器加载的类,则在MyCat
中看不到MySample
。
(2)删除掉MySample
的class
文件,在MySample
的构造方法中打印MyCat
的class
,MySample
由MycalssLoader
加载,MyCat
由AppClassLoader
加载,子加载器能够看见父加载器加载的类,则MySample
可以看到MyCat
的class
。
NO.3
复制一份MyPerson.class
到指定的路径下。
public class MyPerson {
//内部维护一个MyPerson的类型的属性
private MyPerson person;
public MyPerson() {
}
//传进来一个对象,强制转换为MyPerson
public void setPerson(Object o) {
this.person = (MyPerson)o;
}
}
-----------------------------------------
public class MyTest14 {
public static void main(String[] args) throws Exception{
//创建两个MyClassLoader的实例,都去加载位于path路径下的MyPerson.class文件
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader loader2 = new MyClassLoader("loader2");
loader1.setPath("C:\\Users\\Administrator\\Desktop\\jvmTest\\");
loader2.setPath("C:\\Users\\Administrator\\Desktop\\jvmTest\\");
//分别去加载MyPerson.class,得到其class对象
Class<?> clazz1 = loader1.loadClass("classLoader.MyPerson");
Class<?> clazz2 = loader2.loadClass("classLoader.MyPerson");
//比较两个class对象是否相等
System.out.println(clazz1 == clazz2);
//通过class对象,创建实例
Object o1 = clazz1.newInstance();
Object o2 = clazz2.newInstance();
//使用反射的方式去调用MyPerson的setPerson方法
Method setPerson = clazz1.getMethod("setPerson", Object.class);
//调用o1的setPerson方法,将o2传进去。
setPerson.invoke(o1,o2);
}
}
输出结果:
true
通过系统类加载器加载,没什么问题。
从classPath
中删除掉MyPerson.class
文件,再运行程序。
![](http://res.cloudinary.com/cafebabe/58Q_M_T0_WN_1XGCZ_4V6.png)
从结果可以看出,两个
class
对象最终都是由MyClassLoader
来加载得到的,但是得到的class
并不是同一个,并且在执行o1
的setPath
方法时还报错,说无法将MyPerson
转换为MyPerson
,这就很奇怪了?
其实,想想类加载器的命名空间,还是挺简单的。
一个类在Java
虚拟机中的唯一性,是由类与类加载器一起共同决定的,每一个类加载,都有自己独立的命名空间。在此处,loader1
与loader2
虽然都是MyClassLoader
的实例,但是它们之间并不存在双亲委托的关系,即是两个不同的类加载器,即存在两个不同的命名空间,clazz1
和clazz2
属于不同的命名空间。使用反射去调用MyPerson
的serPerson
方法,想把o2
赋值给o1
中的Person
属性,但因为clazz1
和clazz2
是属于不同的命名空间,推广开,o1
和o2
也属于不同的命名空间,两者之间是不可见的,所以不能将o2
赋值给o1
的Person
属性。
总结
通过这个自定义类加载器,我们深入剖析了类加载器的双亲委托机制,这里再放一遍关于类加载器的双亲委托模型的好处:
- 可以确保
Java
核心类库的类型安全:所有的Java应用都至少会引用java.lang.Object
类,也就是说在运行期,java.lang.Object
这个类会被加载到Java虚拟机中;如果这个加载过程是由各自的类加载器去加载的话,那系统中会产生多个版本的Object
类,这些类位于不同的命名空间中,相互之间不兼容,不可见,应用程序将会变得混乱。而通过双亲委托机制,Java
核心类库中的类都由启动加载器来完成加载,从而保证了Java
应用使用的都是同一个Java
核心类库,它们之间是相互兼容的。 - 可以确保Java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以加载相同名称的类,这些相同名称的类可以并存在
Java
虚拟机中。不同类加载器所加载的类是不兼容的,这就相当于在Java
虚拟机中创建了一个又一个的相互隔离的Java
类空间。
最后,提一句,内建于JVM
的启动类加载器会加载java.lang.ClassLoader
以及其他的Java
平台类,当JVM
启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstap
)。