type
status
date
slug
summary
tags
category
icon
password

一、类加载器

1、类加载器的作用

类加载器,顾名思义就是一个可以将 Java 字节码文件加载成内存里面的java.lang.Class实例的工具。类加载过程就是类的初始化过程,包括上一篇文章提到的读取字节数组、验证、解析、初始化等步骤。另外,它也可以加载资源,包括图像文件和配置文件。
类加载器的特点:
  • 动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
  • 全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。
  • 类加载器只负责加载 class 文件,至于是否可以运行,则由 Execution Engine 决定。
  • 加载的类信息存放在方法区里面,每个类都会有指向类加载器的引用,作为类型信息的一部分一起保存在方法区中。
    • notion image

2、类加载器的分类

从实现方式上,类加载器可以分为两种:一种是启动类加载器,由C++语言实现,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器应用程序类加载器以及自定义类加载器。
notion image
  • 启动类加载器Bootstrap ClassLoader):使用C/C++实现,嵌在 JVM 内部,负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib目录中也不会被加载)类库加载到虚拟机内存中。处于安全考虑,启动类加载器只加载包名为 javajavaxsun 等开头的类。
    • 并不继承自 java.lang.ClassLoader ,没有父加载器。
    • 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent,可直接设置 null
  • 扩展类加载器Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,加载 jre/lib/ext 目录下的类库,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定路径中的所有类库。如果用户创建的 jar 放在该目录下也会被自动加载。
    • 启动类加载器加载,父加载器为 Bootstrap 加载器,即parent=null
  • 应用程序类加载器Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,负责加载环境变量 ClassPath 或者系统属性 java.class.path 下的类库,是用户程序中的默认的类加载器。
    • 也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。
    • 开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。
  • 用户自定义类加载器:用户可以通过继承 java.lang.ClassLoader 的方式来实现自己的类加载器。
    • 不建议用户覆盖 loadClass() 方法,因为有可能打破双亲委派机制。建议只覆盖 findClass() 方法定义自己的类加载逻辑。
    • 用户如果没有太复杂的需求,可以直接继承 URLClassLoader 类,这样用户也不需要覆盖 findClass() 方法重写获取字节码流的方式,使自己的类加载器更简洁。
除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader
notion image
这个抽象类中定义了三个关键方法。
从上面的代码可以得到几个关键信息:
  • JVM 的类加载器是分层次的,它们有父子关系,但这个关系不是继承关系,而是组合关系。每个类加载器都持有一个 parent字段,指向父加载器。(AppClassLoaderparentExtClassLoaderExtClassLoaderparentBootstrapClassLoader,但是ExtClassLoaderparent=null。)
  • defineClass 方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
  • findClass 方法的主要职责就是找到.class文件并把.class文件读到内存得到字节码数组,然后调用 defineClass方法得到 Class 对象。子类必须实现findClass
  • loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。

二、双亲委派机制

1、什么是双亲委派机制?

一句话概括:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
notion image
例如定义了类 java.lang.String,不会报错,编译可以通过,但是该类不会被加载,因为 Bootstrap 加载器已经成功加载了 java 核心类库的 java.lang.String 类,所以在执行的时候会报错。
notion image

2、为什么需要双亲委派机制?

  • 避免类的重复加载:双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
  • 保证加载的安全性:可以保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖。例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。

二、破坏双亲委派

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。
只有官方库java.的类必须由启动类加载器加载,无法破坏,扩展类加载器和应用程序类加载器的双亲委派都是可以破坏的破坏的核心是自定义类加载器继承ClassLoader,并重写findClass loadClass 方法。
如果继承 ClassLoader 类,必须实现findClass方法;loadClass 方法是用来保证双亲委派机制的,要破坏双亲委派的话,也需要重写 loadClass 方法。

1、自定义类加载器

1.1 自定义类加载器跳过AppClassLoader和ExtClassLoader

如下是一个自定义的类加载器TestClassLoader,并重写了findClassloadClass
开始测试,初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:
运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?
notion image
转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类ObjectObjectstudy.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。
notion image
运行测试代码:
还是报错了!!!
notion image
报错信息为:Prohibited package name: java.lang
跟了下异常堆栈:
TestClassLoader#findClass最后一行代码调用了java.lang.ClassLoader#defineClass
java.lang.ClassLoader#defineClass最终调用了如下代码:
notion image
notion image
看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。
得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载。
既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可。
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader
💡
双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass
运行测试代码:
notion image
成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。
如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派。

1.2 自定义类加载器加载扩展类

假设classpath下由上述TestClassLoader加载的类中用到了<JAVA_HOME>\lib\ext下的扩展类,那么这些扩展类也会由TestClassLoader加载,但是会报类文件找不到的情况。
但是自定义类加载器也是能加载<JAVA_HOME>\lib\ext下的扩展类的,只要自定义类加载器能找准扩展类的类路径。
以扩展目录com.sun.crypto.provider下的类举例:
(1)Demo中随便引用一个扩展类:
(2)修改TestClassLoader#findClass:
notion image
(3)测试代码中需要调用一下Demo类的构造器:
notion image
(4)运行测试代码
自定义类加载器成功加载了扩展类。
notion image
由上得出结论,<JAVA_HOME>\lib\ext下的扩展类是没有强制只有ExtClassLoader能加载,自定义类加载器也能加载。

1.3 一个比较完整的自定义类加载器

一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:
notion image

2、Tomcat破坏双亲委派机制

Tomcat的类加载器层次结构
notion image

2.1 WebappClassLoader

Tomcat中可以部署多个 Web 项目,假设有两个Web应用程序,它们都有一个类,叫做 User,并且它们的类全限定名都一样,比如都是 com.yyy.User。若使用 JVM 默认的 AppClassLoader 加载 Web 应用,AppClassLoader只能加载一个 User 类,在加载第二个同名 User 类时,AppClassLoader 会返回第一个 User 类的 Class 实例。为了保证每个 Web 项目互相独立,所以不能都由AppClassLoader加载。
所以 Tomcat自定义了一个类加载器 WebAppClassLoader, 并为每个 Web 应用创建一个类加载器实例 WebappClassLoaderWebappClassLoader继承自URLClassLoader,重写了findClassloadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader
notion image
  • 每个 Web 应用自己的 Java 类和依赖的 JAR 包,分别放在WEB-INF/classesWEB-INF/lib目录下,都是 WebAppClassLoader 加载的。
  • Context 容器组件对应一个 Web 应用,因此,每个 Context 容器创建和维护一个 WebAppClassLoader 加载器实例。
  • WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoaderExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

2.2 SharedClassLoader

两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类?
双亲委派机制的各子加载器都能通过父加载器去加载类,于是考虑把需共享的类放到父加载器的加载路径。应用程序即是通过该方式共享 JRE 核心类。
Tomcat 搞了个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,以加载 Web 应用之间共享的类。
WebAppClassLoader 未加载到某类,就委托父加载器 SharedClassLoader 去加载该类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,即可解决共享问题。

2.3 CatalinaClassLoader

如何隔离 Tomcat 本身的类和Web应用的类?
兄弟关系:两个类加载器是平行的,它们可能拥有同一父加载器,但两个兄弟类加载器加载的类是隔离的。
于是,Tomcat搞了CatalinaClassLoader,专门加载Tomcat自身的类。

2.4 CommonClassLoader

问题是,当 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办?
共享依旧靠父子关系。再增加个 CommonClassLoader,作为 CatalinaClassLoaderSharedClassLoader 的父加载器。
总结:CommonClassLoader 能加载的类都可被 CatalinaClassLoaderSharedClassLoader 使用,而 CatalinaClassLoaderSharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

3、线程上下文类加载器

JVM 默认情况下,若一个类由类加载器A加载,则该类的依赖类也由相同的类加载器加载。例如 Class.forName 的源码如下:
看到会使用调用者的类加载器去加载指定类。
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的 rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载ClassPath下的类。
解决方案是线程上下文类加载器。线程上下文类加载器其实是一种类加载器传递机制。可以通过java.lang.Thread#setContextClassLoader方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能把这个类加载器取(java.lang.Thread#getContextClassLoader)出来使用
如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread())中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。
深入 ServiceLoader.load 的源码
第一行,获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类。
线程上下文类加载器的出现就是为了方便破坏双亲委派
有了线程上下文类加载器就好办了,JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

四、常见问题

1、如何判断两个 Class 对象是否同一个类?

  • 全限定类名相同
  • 加载这个类的类加载器相同
即一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()isAssignableFrom()isInstance()以及instanceof关键字等),即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

2、Class.forName 和 ClassLoader.loadClass 的区别

类加载过程:加载——》验证——》准备——》解析——》类初始化——》使用(对象实例初始化)——》卸载。
java.lang.Class.forName 会调用到 forName0 方法,第二个参数 initialize = true,意为会进行类初始化(<clinit>())操作。
java.lang.ClassLoader.loadClass 会调用到 protected 修饰的 loadClass(String name, boolean resolve),第2个参数resolve=false,意为不进行类的解析操作,也就不会进行类初始化,包括静态变量的初始化、静态代码块的运行,都不会进行。

参考

JVM类的实例化JVM类的初始化:类加载的时机和步骤
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏