我们经常会在面试中遇到有关类加载器的问题,而作为一名Java开发人员应该了解类加载器如何工作?双亲委派模型是什么?如何打破双亲委派?为什么打破?等等。所以今天的主题就是聊一聊类加载器。
《深入理解Java虚拟机》这本书大家都不陌生,想必我们大多数人了解JVM知识都是通过这本书,在该书中也详细介绍了Java类加载的全过程,包含 加载、验证、准备、解析和初始化 这5个阶段。
在加载阶段,通过一个类的全限定名来获取此类的二进制字节流,就是依靠类加载器来完成。
类加载器的一个作用就是将编译器编译生成的二进制 Class 文件加载到内存中,进而转换成虚拟机中的类。Java系统提供了三种内置的类加载器:
当然,上面是 Java 默认的类加载器,我们还可以自定义类加载器,后文会分析如何自定义类加载器。
网上有文章分析说,类加载器遵循三个原则: 委托性 、 可见性 和 唯一性原则 。这三点其实都和双亲委派模型有关,双亲委派的工作过程如下:
当类加载器收到类的加载请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求会传送到顶层的启动类加载器,只有父类加载器无法完成加载请求,才会交由子加载器去加载。
三个原则的具体体现是:
聊完双亲委派模型,你肯定想知道它是如何实现,那么来看一下 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; } }
现在我们熟悉了 ClassLoader 的三个重要方法,那么如果需要自定义一个类加载器的话,直接继承 ClassLoader类,一般情况只需要重写 findClass 方法即可,自己定义加载类的路径,可以从文件系统或者网络环境。
但是,如果想打破双亲委派机制,那么还要重写 loadClass 方法,只不过,为什么我们要选择去打破它呢? 我们常使用的 Tomcat的类加载器就打破了双亲委派机制,当然还有一些其他场景也打破了,比如涉及 SPI 的加载动作、热部署等等。
接下来来看看 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 方法。
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; }
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后端知识!