聊聊类加载器与双亲委派模型
前言
我们经常会在面试中遇到有关类加载器的问题,而作为一名Java开发人员应该了解类加载器如何工作?双亲委派模型是什么?如何打破双亲委派?为什么打破?等等。所以今天的主题就是聊一聊类加载器。
ClassLoader 介绍
《深入理解Java虚拟机》这本书大家都不陌生,想必我们大多数人了解JVM知识都是通过这本书,在该书中也详细介绍了Java类加载的全过程,包含加载、验证、准备、解析和初始化这5个阶段。
在加载阶段,通过一个类的全限定名来获取此类的二进制字节流,就是依靠类加载器来完成。
类加载器的一个作用就是将编译器编译生成的二进制 Class 文件加载到内存中,进而转换成虚拟机中的类。Java系统提供了三种内置的类加载器:
启动类加载器 (Bootstrap Class Loader): 负责加载JDK核心类,通常是 rt.jar 和位于 $JAVA_HOME/jre/lib 下的核心库.
扩展类加载器 (Extensions Class Loader): 负责加载\jre\lib\ext目录下 JAR 包
系统类加载器 (System Class Loader):负责加载所有应用程序级别的类到JVM,它会加载classpath环境变量或 -classpath以及-cp命令行参数中指定的文件
当然,上面是 Java 默认的类加载器,我们还可以自定义类加载器,后文会分析如何自定义类加载器。
双亲委派模型是什么
网上有文章分析说,类加载器遵循三个原则:委托性、可见性和唯一性原则。这三点其实都和双亲委派模型有关,双亲委派的工作过程如下:
当类加载器收到类的加载请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求会传送到顶层的启动类加载器,只有父类加载器无法完成加载请求,才会交由子加载器去加载。
三个原则的具体体现是:
「委托性原则」体现在当子类加载器收到类的加载请求时,会将加载请求向上委托给父类加载器。
「可见性原则」体现在允许子类加载器查看父类加载器加载的所有类,但是父类加载器不能查看子类加载器加载的类。
「唯一性原则」体现在双亲委派整个机制保证了Java类的唯一性,假如你写了一个和JRE核心类同名的类,比如Object类,双亲委派机制可以避免自定义类覆盖核心类的行为,因为它首先会将加载类的请求,委托给ExtClassLoader去加载,ExtClassLoader再委托给BootstrapClassLoader,启动类加载器如果发现已经加载了 Object类,那么就不会加载自定义的Object类。
ClassLoader 如何工作
聊完双亲委派模型,你肯定想知道它是如何实现,那么来看一下 ClassLoader 的核心方法,其中的 loadClass 方法就是实现双亲委派机制的关键,为了缩短代码篇幅和方便阅读,去掉了一些代码细节:
package java.lang;
public abstract class ClassLoader {
protected Class defineClass(byte[] b);
protected Class<?> findClass(String name);
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2. 委托给父类加载
c = parent.loadClass(name, false);
} else {
//3. 父类不存在的,交给启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
if (c == null) {
//4. 父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载
c = findClass(name);
}
}
return c;
}
}
defineClass 方法:调用 native 方法将 字节数组解析成一个 Class 对象。
findClass 方法:抽象类ClassLoader中默认抛出ClassNotFoundException,需要继承类自己去实现,目的是通过文件系统或者网络查找类
loadClass 方法:首先根据类的全限定名检查该类是否已经被加载过,如果没有被加载,那么当子加载器持有父加载器的引用时,那么委托给父加载器去尝试加载,如果父类加载器无法完成加载,再交给子类加载器进行加载。loadClass方法 就是实现了双亲委派机制。
现在我们熟悉了 ClassLoader 的三个重要方法,那么如果需要自定义一个类加载器的话,直接继承 ClassLoader类,一般情况只需要重写 findClass 方法即可,自己定义加载类的路径,可以从文件系统或者网络环境。
但是,如果想打破双亲委派机制,那么还要重写 loadClass 方法,只不过,为什么我们要选择去打破它呢?我们常使用的 Tomcat的类加载器就打破了双亲委派机制,当然还有一些其他场景也打破了,比如涉及 SPI 的加载动作、热部署等等。
接下来来看看 Tomcat 为什么打破双亲委派模型以及实现机制。
Tomcat如何打破双亲委派机制
为什么打破
现在都流行使用 springboot 开发 web 应用,Tomcat 内嵌在 springboot 中。而在此之前,我们会使用最原生的方式,servlet + Tomcat 的方式开发和部署 web 程序。web 应用的目录结构大致如下:
| - MyWebApp
| - WEB-INF/web.xml -- 配置文件,用来配置Servlet等
| - WEB-INF/lib/ -- 存放Web应用所需各种JAR包
| - WEB-INF/classes/ -- 存放你的应用类,比如Servlet类
| - META-INF/ -- 目录存放工程的一些信息
一个 Tomcat 可能会部署多个这样的 web 应用,不同的 web 应用可能会依赖同一个第三方库的不同版本,为了保证每个 web 应用的类库都是独立的,需要实现类隔离。而Tomcat 的自定义类加载器 WebAppClassLoader 解决了这个问题,每一个 web 应用都会对应一个 WebAppClassLoader 实例,不同的类加载器实例加载的类是不同的,Web应用之间通各自的类加载器相互隔离。
当然 Tomcat自定义类加载器不只解决上面的问题,WebAppClassLoader 打破了双亲委派机制,即它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用定义的类。
如何打破
WebappClassLoader 具体实现机制是重写了 ClassLoader 的 findClass 和 loadClass 方法。
findClass 方法如下,省去部分细节:
public 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. 先在本地缓存查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
return clazz;
}
//2. 从系统类加载器的缓存中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
//3. 尝试用 ExtClassLoader 类加载器类加载
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索 class 并加载
try {
clazz = findClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// 省略
}
}
//6. 上述过程都加载失败,抛出 ClassNotFoundException 异常
throw new ClassNotFoundException(name);
}
从上面的代码中可以看到,Tomcat 自定义的类加载器确实打破了双亲委派机制,同时根据 loadClass 方法的核心逻辑,我也画了一张图,描述了默认情况下 Tomcat 的类加载机制。
一开始将类加载请求委托给 ExtClassLoader,而不是委托给 AppClassLoader,这样的原因是 防止 web 应用自己的类覆盖JRE的核心类,如果 JRE 核心类中没有该类,那么才交给自定义的类加载器 WebappClassLoader 去加载。
小结
这篇文章主要总结了类加载器的双亲委派模型、双亲委派的工作机制、以及Tomcat如何打破双亲委派,当然有一些东西分享的比较简单,比如 Tomcat 的类加载器这部分,没有提及整个 Tomcat的类加载器层次结构,没有提到 SharedClassLoader 和 CommonClassLoader 类加载器,这个等后续有时间再来分享。
参考资料 & 鸣谢
Java ClassLoader
Class Loaders in Java
How to Use Java Classloaders
Class Loader in Java
How ClassLoader Works in Java
Class Loader HOW-TO——Apache Tomcat 9
极客时间:深入拆解Tomcat & Jetty