前言
什么是类加载器?
类加载器有哪些?
双亲委派机制是怎么样的?
什么时候需要打破双亲委派?
如何打破?
以上内容网上有大量文章介绍一些基础概念,这篇《类加载器、双亲委派机制与打破(Driver、TCCL、Tomcat等)》、《我竟然被“双亲委派”给虐了》有详细介绍,本文着重从源码来讲解下我在理解过程中的疑问
用途
先在前面说,了解这个有啥用,我认为有两点:
- 可以通过网络、数据库、接口等多种形式进行远程class的加载,这个就很有用了,你的代码部署在别人那边,核心class在你这儿
- 代码加密,你的代码部署给用户,自定义加载器来加载解密等操作
- 热部署,运行过程中,直接上传class文件,然后自定义加载,不用重启,spring boot那个热部署插件就使到这个
以上建议还要配合密钥,时间等综合来考虑,不然单一的还是没法保障安全性
疑问
1.jvm默认的类加载器:AppClassLoader、ExtClassLoader、Bootstrap ClassLoader;三者是如何在代码里面没有继承关系,是如何进行逐步委托加载的?
首先他们三不是用extends进行继承操作的,是基于组合进行的松耦合继承,可以看ClassLoader类里面的parent属性,其次rt.jar中
sun.misc.Launcher
类中有两个静态类AppClassLoader
,ExtClassLoader
,查看Launcher的构造函数如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//获取ext类加载器,点进getExtClassLoader()方法可以看到加载的范围为
// String var0 = System.getProperty("java.ext.dirs");
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//获取app类加载器,点进getAppClassLoader()方法可以看到final String var1 = System.getProperty("java.class.path");
//注意此处传入了var1,创建app时,将ext传入,跟进代码可以到顶层抽象类ClassLoader.java,代码见下文
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//注意这个线程上下文类加载器,此加载器可以用户JDBC、spring等打破双亲加载机制,默认的线程上下文加载器 == app加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
private ClassLoader(Void unused, ClassLoader parent) {
//此时this指向app,parent指向ext,且parent为该类的成员变量,这种关系没有使用继承,采用的基于松耦合的组合关系
// 由于启动类加载器是c++实现的,在java的视角里是没有此对象的,所以为null;换而言之,如果this指向是ext,那么他的parent为null,此处在loadClass方法中有体现,见下文代码
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//看看这个findLoadedClass方法的位置,说明相同的类只会加载一次
//这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//父加载器不为空,就一直往上递归,直至交给顶层父类加载器加载,结合上面代码注释可知,从自定义加载器开始,到app、到ext,都会递归往上找parent,直到parent == null
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
//进行递归退出的处理
}
//从顶层开始,每一层类加载器加载不到,就会逐步往下找可以加载的类加载器,所以自定义加载器是需要重写这个findClass方法的
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.双亲委派模型中的"双亲"如何理解?
并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器;
- 父类加载器(parent classloader):它可以替子加载器尝试加载类;
他是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的,他的parent为null,由上文loadclass代码可知,为空就会调用findBootstrapClassOrNull()
。- 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
3.为什么jdbc驱动加载要打破双亲委派
因为Driver接口是java.sql包下的,根据规定,这个应该是bootstrap classloader来加载,但是具体的实现是各个厂商来做的,bootstrap classloader无法加载实现类,这个时候就需要子类加载器加载(加载过程是逐步往上递归,父类加载不到逐步由子类来加载,由此时直接交给app来加载不就行了么,返回为null,直接走app classloader 的findclass方法进行加载不就可以么)
针对上面个问题,重新捋下逻辑
1.jdbc的加载是由DriverManager来执行,这个类在rt下面,由根加载器来加载
2.该类有个静态代码块,里面有loadInitialDrivers()方法,然后跟进方法里面可以看到 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//这儿涉及spi机制来加载厂商的实现
3.厂商的代码肯定不在rt包里面,目前的加载器是根加载器,无法加载厂商实现,jdk的方案是代码如下:public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
可以看到是在当前根加载器执行加载期间,从当前线程获取到上线文加载器,去加载厂商的实现类
我的问题来了:这儿如果不用上下文来加载,直接返回加载不到,return null; 然后直接走app classloader 的findclass方法进行加载不就可以么?
答:被这个问题困扰了几天,想不通,后面猜测如果返回为空,那之前根加载到的DriverManager怎么办,是直接放弃了么,我理解的是整个DriverManager加载和厂商驱动加载是一体的原子性的,必须要在根加载器加载到一半的时候继续去加载厂商的,如果这个时候返回为空,那么后面app是可以加载厂商实现,但是DriverManager怎么办,不加载了么,要加载的话,又是根加载器来加载
这个理解不知道对不对
4.为什么tomcat要打破双亲委派模型的?
从问题1我们知道,类的加载是逐步委托给顶层的类加载器进行加载,加载不到时才会由下面的加载器进行加载;
假设我们tomcat容器中有三个server,server1的jackson是1.0;server2的jackson是2.0;server3的jackson是3.0;
三个版本里面的某个类的方法里面的实现可能不一样,但是由于双亲委派机制的存在,都会交由顶层类加载器来加载,如果没有自定义,那么应该是app加载,就会导致该类最终会被覆盖成某个版本;(类在jvm中的唯一性由类加载器名和类全路径决定)
因为我们的三个server是不同的应用,所以我们需要各个server进行隔离,独自加载,不应该使用双亲委派交由顶层加载
- tomcat是如何打破双亲委派模型的?
首先推荐一篇优秀的文章 [Tomcat 的架构有哪些牛逼之处?](https://mp.weixin.qq.com/s/_bsAOTA10fGDJsz2jCYivg) 从架构、实现等多个维度层面进行分析,文章较难,需要多次阅读
org.apache.catalina.loader.WebappClassLoader
,该类重写了findclass方法
ublic Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1. 先在 Web 应用目录下查找类
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父类也没找到,抛出 ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
同样的也重写了loadclass方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地 cache 查找该类是否已经加载过(主要是tomcat自定义加载器中查找)
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 从系统类加载器的 cache 中查找是否加载过(从jvm加载的类里面查找)
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用 ExtClassLoader 类加载器类加载(此处是精髓,注意是ext,不是app,基础类和扩展类都是交由双亲来加载,避免了覆盖 JRE 核心类,保证虚拟机的正常运行)
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索 class 并加载(这儿就是tomcat自定义加载器加载了)
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
(其他类还有交由app加载)
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}

-
WebAppClassLoader
Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context容器负责创建和维护一个 WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。 -
SharedClassLoader
本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗。
因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader自己没有加载到某个类,就会委托父加载器 SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。 -
CatalinaClassloader
如何隔离 Tomcat 本身的类和 Web 应用的类?
要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。
这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader和 SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被 CatalinaClassLoader和 SharedClassLoader使用
//TODO
6.类加载成功后会立即调用构造函数进行实例化么
不会,如果涉及到new等创建对象才会实例化,类加载后是会初始化静态代码块,静态变量等,类的初始化和对象的初始化是两个事情
7.自定义类加载器是如何保障加载器执行顺序的,即app怎么成为自定义加载器的parent
ClassLoader类里面默认的parent是app
protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }
8.当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式
总结:
- 参考classloader里面申明 private final ClassLoader parent;然后子类基于组合的集成方式,之后递归双亲委派【从这儿我们可以抄作业,就是我们设计底层框架,借鉴这个模式,从而实现父类调用子类的方
- tomcat架构运用模板方法模式。分别运用了组合模式、观察者模式、骨架抽象类和模板方法,需要自己去体会如何使用的,使用和不使用的区别
网友评论