vlambda博客
学习文章列表

聊聊类加载器与双亲委派模型

前言

我们经常会在面试中遇到有关类加载器的问题,而作为一名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 方法就是实现双亲委派机制的关键,为了缩短代码篇幅和方便阅读,去掉了一些代码细节:

 
   
   
 
  1. package java.lang;

  2. public abstract class ClassLoader {


  3. protected Class defineClass(byte[] b);


  4. protected Class<?> findClass(String name);


  5. protected Class<?> loadClass(String name, boolean resolve) {

  6. synchronized (getClassLoadingLock(name)) {

  7. // 1. 检查类是否已经被加载过

  8. Class<?> c = findLoadedClass(name);

  9. if (c == null) {

  10. try {

  11. if (parent != null) {

  12. //2. 委托给父类加载

  13. c = parent.loadClass(name, false);

  14. } else {

  15. //3. 父类不存在的,交给启动类加载器

  16. c = findBootstrapClassOrNull(name);

  17. }

  18. } catch (ClassNotFoundException e) { }

  19. if (c == null) {

  20. //4. 父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载

  21. c = findClass(name);

  22. }

  23. }

  24. return c;

  25. }

  26. }

  • defineClass 方法:调用 native 方法将 字节数组解析成一个 Class 对象。

  • findClass 方法:抽象类ClassLoader中默认抛出ClassNotFoundException,需要继承类自己去实现,目的是通过文件系统或者网络查找类

  • loadClass 方法:首先根据类的全限定名检查该类是否已经被加载过,如果没有被加载,那么当子加载器持有父加载器的引用时,那么委托给父加载器去尝试加载,如果父类加载器无法完成加载,再交给子类加载器进行加载。loadClass方法 就是实现了双亲委派机制。

现在我们熟悉了 ClassLoader 的三个重要方法,那么如果需要自定义一个类加载器的话,直接继承 ClassLoader类,一般情况只需要重写 findClass 方法即可,自己定义加载类的路径,可以从文件系统或者网络环境。

但是,如果想打破双亲委派机制,那么还要重写 loadClass 方法,只不过,为什么我们要选择去打破它呢?我们常使用的 Tomcat的类加载器就打破了双亲委派机制,当然还有一些其他场景也打破了,比如涉及 SPI 的加载动作、热部署等等。

接下来来看看 Tomcat 为什么打破双亲委派模型以及实现机制。

Tomcat如何打破双亲委派机制

为什么打破

现在都流行使用 springboot 开发 web 应用,Tomcat 内嵌在 springboot 中。而在此之前,我们会使用最原生的方式,servlet + Tomcat 的方式开发和部署 web 程序。web 应用的目录结构大致如下:

 
   
   
 
  1. | - MyWebApp

  2. | - WEB-INF/web.xml -- 配置文件,用来配置Servlet

  3. | - WEB-INF/lib/ -- 存放Web应用所需各种JAR

  4. | - WEB-INF/classes/ -- 存放你的应用类,比如Servlet

  5. | - META-INF/ -- 目录存放工程的一些信息

一个 Tomcat 可能会部署多个这样的 web 应用,不同的 web 应用可能会依赖同一个第三方库的不同版本,为了保证每个 web 应用的类库都是独立的,需要实现类隔离。而Tomcat 的自定义类加载器 WebAppClassLoader 解决了这个问题,每一个 web 应用都会对应一个 WebAppClassLoader 实例,不同的类加载器实例加载的类是不同的,Web应用之间通各自的类加载器相互隔离。

当然 Tomcat自定义类加载器不只解决上面的问题,WebAppClassLoader 打破了双亲委派机制,即它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用定义的类。

如何打破

WebappClassLoader 具体实现机制是重写了 ClassLoader 的 findClass 和 loadClass 方法。

  • findClass 方法如下,省去部分细节:

 
   
   
 
  1. public Class<?> findClass(String name) throws ClassNotFoundException {

  2. ...

  3. Class<?> clazz = null;

  4. try {

  5. //1. 先在 Web 应用目录下查找类

  6. clazz = findClassInternal(name);

  7. } catch (RuntimeException e) {

  8. throw e;

  9. }

  10. if (clazz == null) {

  11. try {

  12. //2. 如果在本地目录没有找到,交给父加载器去查找

  13. clazz = super.findClass(name);

  14. } catch (RuntimeException e) {

  15. throw e;

  16. }

  17. }

  18. //3. 如果父类也没找到,抛出 ClassNotFoundException

  19. if (clazz == null) {

  20. throw new ClassNotFoundException(name);

  21. }

  22. return clazz;

  23. }

  • loadClass方法如下,省去部分细节:

 
   
   
 
  1. public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

  2. synchronized (getClassLoadingLock(name)) {

  3. Class<?> clazz = null;


  4. //1. 先在本地缓存查找该类是否已经加载过

  5. clazz = findLoadedClass0(name);

  6. if (clazz != null) {

  7. return clazz;

  8. }

  9. //2. 从系统类加载器的缓存中查找是否加载过

  10. clazz = findLoadedClass(name);

  11. if (clazz != null) {

  12. return clazz;

  13. }

  14. //3. 尝试用 ExtClassLoader 类加载器类加载

  15. ClassLoader javaseLoader = getJavaseClassLoader();

  16. try {

  17. clazz = javaseLoader.loadClass(name);

  18. if (clazz != null) {

  19. return clazz;

  20. }

  21. } catch (ClassNotFoundException e) {

  22. // Ignore

  23. }

  24. // 4. 尝试在本地目录搜索 class 并加载

  25. try {

  26. clazz = findClass(name);

  27. if (clazz != null) {

  28. return clazz;

  29. }

  30. } catch (ClassNotFoundException e) {

  31. // Ignore

  32. }

  33. // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载

  34. try {

  35. clazz = Class.forName(name, false, parent);

  36. if (clazz != null) {

  37. return clazz;

  38. }

  39. } catch (ClassNotFoundException e) {

  40. // 省略

  41. }

  42. }

  43. //6. 上述过程都加载失败,抛出 ClassNotFoundException 异常

  44. throw new ClassNotFoundException(name);

  45. }

从上面的代码中可以看到,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