说起双亲委派,首先需要了解下面三类类加载器。
1.引导类加载器
负责加载支撑jvm运行的位于jre的Lib目录下的核心类库,比如rt.jar,charsets.jar等。
比如String.class.getClassLoder()获取到的就是引导类加载器。但是引导类加载器是通过c++实现的,在java代码运行时无法获取到引导类加载器的信息,即获取到的是null。
2.扩展类加载器
负责加载支撑Jvm运行的位于jre的Lib目录下的ext扩展目录中的jar类包。
sun.misc.Launcher#ExtClassLoader
3.应用程序类加载器
负责加载classpath路径下的类包,即用户自己实现的那些类。
sun.misc.Launcher#AppClassLoader
另外,开发者还可以编写自定义类加载器,用来负责加载用户自定义路径下的类包。
双亲委派是什么?
所谓双亲委派是指,当应用程序使用到某个类时,首先检查应用程序类加载器的加载列表里是否存在这个类,即检查该类是否被加载过。
如果被加载过,则返回。如果没有被加载过,则获取扩展类加载器并检查扩展类加载器的加载列表是否存在这个类。逐层网上,最后委托到引导类加载器。
引导类加载器会尝试加载此类,也就是会进到jre的lib目录下去检查是否存在这个类并进行加载。
如果lib目录下不存在这个类,再往下委托给扩展类加载器。
扩展类加载器再进到lib目录的ext目录下去寻找这个类并进行加载。
如果找不到,再往下委托给应用程序类加载器。应用程序类加载器再到classpath下去寻找这个类并进行加载。
为什么会有先向上,再向下的一个搜索过程?
首先,真正的设计思想只有jvm虚拟机的实现者才知道,而我们只能去理解这样设计的意义。
比如,应用程序中百分之九十五的类都是用户自己实现的,并且每个类都会在很多地方使用很多次,如果一开始就使用引导类加载器去加载类的话,每次用到这个类都会导致一个自上而下的搜索过程,效率会降低。
反之,如果一开始就使用应用程序类加载器去加载的话,只会在第一次出现向上再向下的过程,由于这个类加载之后会被保存到应用程序类加载器的加载列表中,之后每次用到这个类,则能快速确定这个类是否已经被加载过,不会再出现向下搜索的过程,效率反而会提高。
为什么要设计双亲委派?
1.沙箱安全机制,防止核心API库被篡改。
比如重写java.lang.String类,就会无法加载当前重写的String类。因为jdk自带的String类已经被加载。
2.避免类被重复加载
当父加载器已经加载了该类时,没有必要再让子ClassLoader再加载一次,保证被加载类的唯一性。
全盘委托机制
当一个ClassLoader装载一个类时,除非显示使用另外一个ClassLoader,否则该类所依赖及引用的类也由这个ClassLoader载入。
比如Math类里,引用到了User类,ClassLoader装载Math类之后,运行时发现需要装载User类,那么也会通过Math的类加载器去装载User类。
自定义类加载器
自定义类加载器只需要继承java.lang.ClassLoader类。
该类主要有两个核心方法,一个是loadClass方法,它实现了双亲委派机制。
还有一个是findClass方法,默认实现是空方法,所以自定义类加载器主要是重写findClass方法。findClass方法是真正实现加载类的处理。
注意,自定义类加载器的默认父类加载器是应用程序类加载器。
在构造自定义类加载器时会先构造父类ClassLoader,父类ClassLoader在构造时会传入一个parent,这个parent通过getSystemClassLoader获得。
getSystemClassLoader获取的是Launcher实例中的this.classLoader。
而创建Launcher实例时,this.classLoader就被指定成了appClassLoader。
这样做的主要目的也是实现双亲委派机制。
打破双亲委派
即加载某个类时,不再委托给双亲,而是通过自己实现的ClassLoader来加载这个类。
双亲委派的实现主要是在loadClass方法中,所以重写loadClass方法时,只需要把loadClass方法中的双亲委派代码给删掉就行。
注意,jdk核心的类都不允许通过自定义类加载器去进行加载,因为会触发jvm的安全机制,禁止加载。针对此类问题的解决办法就是,针对业务场景,识别需要打破双亲和不需要打破双亲的类,来进行复写loadClass方法。比如如果需要加载jdk核心类时则依然使用双亲委派,如果需要加载非jdk核心类时则打破双亲委派。
打破双亲委派的意义
以tomcat举例,同一个tomcat实例部署多个Web应用程序时,假设一个应用程序引用spring4,另一个程序引用spring5,如果不打破双亲委派的话,那么应用程序对第三方的引用库就会出现混乱。
因此,出现了这样一个需求,jdk的核心库仍然由引导类加载器和扩展类加载器去进行加载。但是应用程序类路径下的类则需要由应用程序自己实现的类加载器去进行加载,并且多个应用程序之间,类路径下的类加载是相互隔离,并且还需要共存。
针对该需求,Tomcat加载tomcat公共类库时共用一个自定义公共类加载器。而针对不同的应用程序时,为各个应用程序生成一个自定义类加载器WebappClassLoader的实例。
而WebappClassLoader就打破了双亲委派,虽然WebappClassLoader的父加载器是自定义公共类加载器,但是它不会向上委托,只加载自己类路径下的类库。当然,公共的类库还是向上委托的。
Tomcat的热加载
热加载的实现方式有很多种。
比如比较文件时间戳,后台有一个线程时刻检测文件中class文件的变动,一旦某个class文件出现变化,则把这个类对应的classLoader给置空,然后重新生成一个classLoader,重新加载变动后的class文件。
本文详细解释了Java中的双亲委派机制,包括引导、扩展和应用程序类加载器的作用,以及为何设计双亲委派以提高效率和保证安全。同时讨论了如何打破双亲委派在特定场景下的应用,如Tomcat中为避免类库冲突的热加载策略。
2362





