Java类加载系列文章:
“显微镜”下的JVM类加载系列(一):”手术刀”剖析过的JVM类加载
“显微镜”下的JVM类加载系列(二):”玩弄”形形色色的类加载器
“显微镜”下的JVM类加载系列(三):类加载的”独奏”-双亲委派模式
前世的回忆
- 为什么不同的类加载器的读取类路径不同
- 每个类加载器的parent属性到底有什么用以及为什么在不同的类加载器之前设立这种父子关系
在这篇文章中,这两个问题都是体现着双亲委派机制的特点,所以在这篇文章中,我们就好好的把玩双亲委派机制
双亲委派机制
我们先根据下方的流程图来讲下双亲委派机制的流程,再根据这个流程,由全局到细节的去看双亲委派机制
双亲委派机制流程
双亲委派机制说白了就是父加载器加载失败就有子类加载器自己加载这么一句话,但是这句话肯定会让很多人都一脸懵逼,所以我们通过TestJDKClassLoader为例再结合流程图来讲下双亲委派机制的流程:
public class TestClassLoader {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
类的加载流程
TestClassLoader就是我们普普通通的应用程序类,我们就以这个类作为例子,结合流程图来说下他的加载过程:
第一步:由于TestClassLoader是应用程序类,所以它会由我们AppClassLoader也就是应用程序类加载器开始加载,发现如果这个类已经被加载过,那么直接返回,如果没有那么就委托ExtClassLoader类加载器进行加载
第二步:ExtClassLoader类加载器开始加载,发现如果这个类已经被加载过,那么直接返回,如果没有那么就委托bootStrapLoader进行加载
第三步:BootStrapLoader(引导类类加载器)开始加载,发现如果这个类已经被加载过,那么直接返回,如果没有,那么就尝试着进行加载
第四步:BootStrapLoader开始加载,他会去第二篇文章中打印的BootStrapLoader加载路径中去寻找,因为TestJDKClassLoader类是在我们的应用程序中,jdk的核心包里当然没有,所以必然会失败,失败之后它就会委托ExtClassLoader类加载器进行加载
第五步:ExtClassLoader类加载器接受BootStrapLoader委托,从lib\ext路径中去寻找,当然也会找不到,也会失败,失败之后它就会委托应用程序类加载器进行加载
第六步:AppClassLoader类加载器接受委托,从定义的应用程序的classPath路径下进行寻找,当然就会找得到,找到之后就会开始进行加载
所以这是一个自下而上再而下的这么一个过程:
AppClassLoader—->ExtClassLoader—->BootStrapLoader—->ExtClassLoader—->AppClassLoader
加载流程带来的问题
我不知道小伙伴有没有这样的疑问:为什么类的加载器是自下而上进行委托,而不是由上到下或者其他顺序呢?因为如果直接从上到下开始加载只需要走一遍,如果从下到上再到下,就重复了一遍,那么这一遍到底有没有必要呢?我们就带着这个问题来看看源码:
还记得这张图吗?类加载就是从launcher.getClassLoader获取类加载器,再通过类加载器的loadClass方法进行类的加载,我们首先就从getClassLoader方法开始:
launcher.getClassLoader
这个loader就是我们在初始化Launcher的时候赋值的AppClassLoader
从源码中我们就可以看出:
所有的类的加载流程都保持统一,都是从AppClassLoader类加载器开始进行加载
- 那JVM为什么要从AppClassLoader开始进行加载呢?
- 为什么这里不直接返回BootStrapLoader类加载器?这样的话不就避免了一次向上委托的流程了吗?性能同时反而会更好
猜测
在我们的日常应用中,90%以上的类都是在classPath的目录下,虽然在第一次加载的时候,会重复一轮委托,但是如果有第二次、第三次、第四次等等重复加载一个类的时候(手动加载的场景),AppClassLoader判断如果已经加载的话就会直接返回,避免向上委托,如果从BootStrapLoader类加载器开始判断,那么无论重复几次,都需要由上往下走一遍,虽然第一次是快了,但是之后的每一次实际上都慢了。
双亲委派机制加载的实现
双亲委派机制是通过下面的loadClass方法实现的,我们进到这个方法的源码看下:
sun.misc.Launcher.AppClassLoader#loadClass
loadClass该方法上面都是一些校验,这里不再详细展开,它也就是AppClassLoader最后调用了父类的loadClass方法
// classLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果当前加载器的父加载器不为空则委托父类类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
分析loadClass方法
- 第一步:它首先调用本地方法findLoadedClass,如果已经加载就返回该类,如果没有返回返回null,紧接着就在findLoadedClass外部判断,如果不为null说明已经加载过,就直接返回,如果没有就进入到第二步
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
- 第二步:如果当前类加载器没有加载,那么就会委托给父加载器,接着来看第三步
当前是AppClassLoader,父加载器是ExtClassLoader,所以这里肯定不为空,那么调用父类加载器的loadClass方法,注意:这一步是一个嵌套方法,我们只是先分析父类的loadClass方法
// 如果当前加载器的父加载器不为空则委托父类类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
}
- 第三步:进入到父类加载器的loadClass方法之后,还是一样的套路,首先还是判断当前类是否已经加载,如果已经加载,那么直接返回,如果没有加载,那么就判断父加载器,但是ExtClassLoader的父加载器是BootStrapLoader,但是这个BootStrapLoader是用C++语言实现的,在Java代码中,ExtClassLoader的parent是null,所以进入到findBootstrapClassOrNull方法中,findBootstrapClassOrNull这个方法就是委托给BootStrapLoader类加载器
// 如果当前加载器的父加载器不为空则委托父类类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 判断BootstrapClassLoader是否加载了该类,如果没有加载,直接返回null
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// 如果没有找到,那么直接返回null
private native Class<?> findBootstrapClass(String name);
第四步:TestJDKClassLoader是我们自己编写的应用程序类,class路径是在我们自定义的,必然不在ExtClassLoader和BootStrapLoader类加载器的扫描路径下,这两个类加载器当然加载不到,也就是上图中的变量“c”肯定是null第一次加载的时候
第五步:判断如果c是null意味着BootStrapLoader找不到,那么就调用ExtClassLoader的findClass方法进行查找,findClass是一个模板方法,并且在ExtClassLoader中是没有重写的,但是在它的父类URLClassLoader中却是有实现这一步就是前文提到的,如果BootStrapLoader找不到的话,会委托给ExtClassLoader进行加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
- 第六步:整个findClass最核心的部分就是用红框框框起来的部分,首先它会拼出类的路径(把”.”替换成”/“,再后缀加上class),再根据这个类路径,从当前类加载器负责的类路径下开始寻找,如果寻找到就执行defineClass方法,如果没有就直接返回null
篇外小剧场
虽然ExtClassLoader的findClass肯定不会走到defineClass这个方法,但是这个方法很重要,它含有大量的本地方法,这个方法做的事情就是我们这个系列第一篇文章中所说的类的加载过程,加载->验证->准备->解析->初始化这五步,我们在这里不继续跟进去了(如果跟进去要翻HotSpot 源码了,这一章中我们先讲双亲委派机制)
第七步:显而易见,ExtClassLoader不会加载我们应用程序的TestJDKClassLoader类,所以最后,ExtClassLoader的loadClass返回的还是null
第八步:还记得这是一个嵌套方法吗?ExtClassLoader的loadClass方法返回出去会回到AppClassLoader这就是我们上文说的,ExtClassLoader如果加载不到的话会委托AppClassLoader的意思,ExtClassLoader的loadClass返回的是null,那么在AppClassLoader的loadClass方法中,也会进入以下这段代码
- 第九步:这段代码中,也同样的会去调用findClass方法,同样的,执行的代码是相同的,但是不同的是AppClassLoader是能扫描到我们的TestJDKClassLoader类,所以这里的res不为空,就会执行defineClass方法进行类的加载
至此,就是我们完成的一个双亲委派机制的流程,从AppClassLoader开始委托给ExtClassLoader再委托给BootStrapLoader开始进行加载,BootStrapLoader如果加载不到再委托给ExtClassLoader,ExtClassLoader加载不到再委托给AppClassLoader,这样一个自下而上再而下的这么一个过程
但是JVM为什么要这样设计呢?这样设计的好处是什么?下面就开始讲双亲委派机制的好处
双亲委派机制的优缺点
优点
- 沙箱安全机制:不同的类由不同的类加载器进行加载,保护Java核心的类不被随意的修改
场景:在应用程序下新建java.lang包,在包下面新建String类,在String类中运行main方法
分析输出结果:
因为在双亲委派机制中,向上委托到BootStrapLoader类加载器的时候,发现java.lang.String在它扫描的路径下,所以它会去加载(只认文件名),但是它加载的不是我们自定义的String类,加载的是rt包下的String类,而java自带的String类是没有main方法的,这里就会报错,很好的保护了Java核心的类库不被随意的修改
- 避免类的重复加载:当父加载器已经加载了该类时,没有必要子类的classLoader再去加载一遍,保证被加载的类的唯一性
篇外小剧场:关于全盘委托
“全盘负责”是指当一个ClassLoader加载一个类时,发现这个类依赖了其他类,那么这个它依赖的类也是由当前的类加载器加载
缺点
双亲委派这种类的加载模式也不是适用于所有的场景
举个例子:双亲委派的一个很鲜明的特点就是相同路径下的类只会被加载一次,但是在Tomcat中,如果一个Tomcat想部署多个应用,而这多个应用恰巧依赖了不同小版本之间的Spring,比如Spring4.1x、Spring4.2x,这两个微小版本的Spring肯定会有相同路径的类,但是如果使用双亲委派机制的类加载器,这两个相同路径下的类只会被加载一个,其中一个应用正好运用到了另外一个类的某些特性,所以必然会导致应用无法正常执行,所以双亲委派机制在Tomcat场景中肯定不适用,就必须打破这种双亲委派机制
自定义类加载器
为了打破双亲委派机制,就肯定不能使用原生的类加载器原生的类加载器一定会使用双亲委派机制,所以就必须自定义类加载器
自定义加载器只需要继承java.lang.ClassLoader类,这个类里面有两个核心的方法,一个是loadClass方法,这个方法中实现了我们的双亲委派机制,还有一个就是findClass方法,默认是空实现,这个方法的定义就是根据路径找到我们的类并进行加载,打破双亲委派机制只需要重写loadClass方法就可以了
编写自定义类加载器
我们先只重写findClass方法后来分析下面这个自定义类加载器,来解释为什么一定要重写loadClass方法
public class MyClassLoaderTest {
// 自定义类加载器一般都需要继承一个ClassLoader(有很多方法可以复用)
static class MyClassLoader extends ClassLoader {
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
/**
* 自定义读取文件方法
* @param name 文件路径名
* @return 读取的二进制数据
* @throws Exception 一场
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classpath + "/" + name + ".class");
int len = fileInputStream.available();
byte[] data = new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 自定义类的读取方法
byte[] data = loadByte(name);
// defineClass 方法只是类的加载,用原生的即可
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
// D盘创建 test/classLoader/TestJDKClassLoader 将TestJDKClassLoader.class丢入该目录
Class clazz = classLoader.loadClass("classLoader.TestJDKClassLoader");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
我们看下输出结果:
I can fly
sun.misc.Launcher$AppClassLoader
分析一下输出结果同时,这里有一个小疑问:
既然我们编写了自定义的类加载器,那么为什么使用自定义类加载器加载的时候,发现TestJDKClassLoader类却是被AppClassLoader类加载器加载的,我们带着这个疑问来继续看
自定义类加载器的父类加载器
在上篇文章中有说道:每个类都有一个parent属性,那么我们自定义类加载器的parent属性存储的是什么呢?来看下源码:
我们发现在初始化自定义类加载器的时候,由于它继承ClassLoader,所以会先去调用父类的构造函数,在父类的构造函数中,默认的塞了一个系统类加载器
跟到getSystemClassLoader方法中,发现它调用的还是Launcher.getClassLoader,而这个方法返回的就是AppClassLoader
所以,这一顿分析其实解释了上面的问题:
- 自定义的类加载器默认情况下的父类是AppClassLoader,由于没有重写loadClass方法,也就是说没有打破双亲委派机制,所以还是执行了双亲委派模式
- 在项目中没有把TestJDKClassLoader这个类给删除掉,也就是说AppClassLoader还是能读到
下面我们删除项目里的TestJDKClassLoader类,并且在D:/test/classLoader目录下创建TestJDKClassLoader.class文件,我们再次输出结果,发现就变成自定义类加载器
I can fly
classLoader.MyClassLoaderTest$MyClassLoader
注意:虽然输出了自定义类加载器,但还是走了双亲委派模式,只是父类AppClassLoader再它的路径下找不到TestJDKClassLoader.class文件而已,为了打破双亲委派模式,我们继续往下看:
打破双亲委派机制
我们还是需要重写loadClass方法,就不委托给父类尝试着进行加载了,直接在当前的类加载器进行加载,所以我们重写下loadClass
public class MyClassLoaderTest {
// 自定义类加载器一般都需要继承一个ClassLoader(有很多方法可以复用)
static class MyClassLoader extends ClassLoader {
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
/**
* 自定义读取文件方法
* @param name 文件路径名
* @return 读取的二进制数据
* @throws Exception 一场
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classpath + "/" + name + ".class");
int len = fileInputStream.available();
byte[] data = new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// 发现如果当前类加载器没有加载过,那么就去加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 自定义类的读取方法
byte[] data = loadByte(name);
// defineClass 方法只是类的加载,用原生的即可
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
// D盘创建 test/classLoader/TestJDKClassLoader 将TestJDKClassLoader.class丢入该目录
Class clazz = classLoader.loadClass("classLoader.TestJDKClassLoader");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.setAccessible(true);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
这是我们加了loadClass方法之后的自定义类加载器,我们把TestJDKClassLoader粘贴到应用程序后运行试试看:
发现报了上面的错,大家可以先不看下面的答案,自己尝试分析一下:
问题答案
- 一方面:因为在java中,默认情况下每个类都会继承Object类,但是Object.class文件是不存在test/classLoader目录下
- 另一方面:Java也不会允许核心的包用自定义的类加载器加载要是不信可以自己把Object.class拖出来放到对应目录下,会报安全错误,这里就不贴图了
过滤特殊的类
为了能让Object类加载,我们还得在loadClass做一个简单的过滤,保证特定的类类似Object还是走双亲委派机制,自己应用程序的类就打破双亲委派机制:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果是指定目录下的文件,那么就打破双亲委派机制
if (name.startsWith("classLoader")) {
c = findClass(name);
} else {
// 否则就走双亲委派机制
c = this.getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我们运行一下看看:
I can fly
classLoader.MyClassLoaderTest$MyClassLoader
可以看到即使在项目中存在了classLoader.MyClassLoaderTest类,还是使用了自己的类加载器加载,没用父类的AppClassLoader类加载器,证明已经打破了双亲委派机制
打破双亲委派机制的场景
打破双亲委派机制的场景有不少,典型的就是Tomcat
一个Tomcat可能部署多个应用,不同的应用可能依赖的同一个第三方类库的不同版本(会造成很多大量的文件路径相同的类),这种情况下就不能通过双亲委派机制去加载,要保证每个应用的类库是独立的,相互隔离
web容器要支持jsp修改,jsp文件最终也需要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp是一件高频的事情,web容器需要支持jsp修改后无需重启
打破双亲委派机制的场景还有很多,大家感兴趣的可以网上找下相关知识点,这里就不再详细展开
本文总结
好啦,以上就是这篇文章的全部内容了,我们一起来回忆一下:
- 双亲委派机制流程的详细介绍
- 双亲委派机制实现源码分析
- 双亲委派机制的优缺点
- 怎么编写自定义的类加载器
- 如何打破双亲委派机制
看到这,我相信各位看官都能回答篇头的两个问题,那么关于类加载也就告一段落了,但是这不是类加载的终点,后面分析Tomcat源码的时候,其中有一块就是自定义类加载器在Tomcat中的应用,这里就不适合详细展开,小伙伴可以期待一下