type
status
date
slug
summary
tags
category
password

一、类加载器

1、类加载器是什么

类加载器的作用是负责类加载过程的工具,即负责将 Java 字节码文件(.class 文件)加载到内存,经过数据验证、解析、初始化等步骤,最后转换成可以被 JVM 执行的方法区的运行时数据结构(java.lang.Class 对象)。
类加载器的特点:
  • 动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包 jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
  • 全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。
  • 类加载器只负责加载 .class 文件,至于是否可以运行,则由 Execution Engine 决定。
  • 加载的类信息存放在方法区里面,每个类都会有指向类加载器的引用,作为类型信息的一部分一起保存在方法区中。
    • notion image

2、类加载器的分类

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

二、双亲委派机制是什么

双亲委派机制是指:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
notion image
为什么需要双亲委派机制?
  • 保证 Java 核心 API 的安全性:防止核心 API 被开发者篡改,保证java官方的类库<JAVA_HOME>/jre/lib和扩展类库<JAVA_HOME>/jre/lib/ext的加载安全性。例如用户自定义了一个java.lang.String 类,编译的时候不会报错,但是该类不会被加载,因为 Bootstrap 加载器已经成功加载了 java 核心类库的 java.lang.String 类,所以在执行的时候会报错。
    • notion image
  • 避免类的重复加载:保证一个类在各个类加载器中都是同一个类。例如 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 为什么要破坏双亲委派机制呢?
  • 应用隔离需求:Tomcat 中可以部署多个 Web 项目,假设有两个 Web 应用程序,它们都有一个类,叫做 User,并且它们的类全限定名都一样,比如都是 com.yyy.User。若使用 JVM 默认的 AppClassLoader 加载 Web 应用,AppClassLoader只能加载一个 User 类,在加载第二个同名 User 类时,AppClassLoader 会返回第一个 User 类的 Class 实例。为了保证每个 Web 项目互相独立,所以不能都由AppClassLoader加载。
  • 热部署支持:不重启 JVM 的前提下能够重新加载修改后的类,标准机制无法实现类的动态替换。
为此 Tomcat 实现了自己的类加载器层次体系,如下图所示:
notion image
  • WebappClassLoader:Tomcat 为每个 Web 应用创建一个类加载器实例 WebappClassLoader。每个 Web 应用自己的 Java 类和依赖的 JAR 包,分别放在WEB-INF/classesWEB-INF/lib目录下,都是 WebAppClassLoader 加载的。WebappClassLoader继承自URLClassLoader,重写了findClassloadClassWebappClassLoader的父类加载器是CommonClassLoader
  • SharedClassLoader:两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类?双亲委派机制的各子加载器都能通过父加载器去加载类,于是考虑把需共享的类放到父加载器的加载路径。应用程序即是通过该方式共享 JRE 核心类。Tomcat 搞了个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,以加载 Web 应用之间共享的类。若 WebAppClassLoader 未加载到某类,就委托父加载器 SharedClassLoader 去加载该类,SharedClassLoader 会加载指定目录($CATALINA_HOME/shared/lib)下的共享类,之后返回给 WebAppClassLoader,即可解决共享问题。
  • CatalinaClassLoader:如何隔离 Tomcat 本身的类和 Web 应用的类?两个类加载器是平行的,它们可能拥有同一父加载器,但两个兄弟类加载器加载的类是隔离的。于是,Tomcat 搞了CatalinaClassLoader,专门加载 Tomcat 自身的类。
  • CommonClassLoader:当 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办?共享依旧靠父子关系。再增加个 CommonClassLoader,作为 CatalinaClassLoaderSharedClassLoader 的父加载器。
总结:
  • CommonClassLoader 能加载的类都可被 CatalinaClassLoaderSharedClassLoader 使用。
  • CatalinaClassLoaderSharedClassLoader 能加载的类则与对方相互隔离。
  • WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

3、线程上下文类加载器

JVM 默认情况下,若一个类由类加载器 A 加载,则该类的依赖类也由相同的类加载器加载。例如 Class.forName 的源码如下,看到会使用调用者的类加载器去加载指定类。
但这种模型在某些场景下存在问题,例如:
  • SPI(Service Provider Interface)场景:基础接口由核心类库定义(如JDBC、JNDI 等),但实现由第三方提供。例如 JNDI 是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3 时放进去的 rt.jar),但 JNDI 的实现由独立厂商实现并部署在应用程序的 ClassPath 下,但启动类加载器不可能去加载 ClassPath 下的类。
  • 反向依赖问题:高层类需要加载低层类时,传统的委派模型无法满足。
解决方案是线程上下文类加载器线程上下文类加载器为每一个线程设置上下文类加载器(通过java.lang.Thread#setContextClassLoader 方法),在该线程后续执行过程中再把这个类加载器取出来使用(通过java.lang.Thread#getContextClassLoader 方法)。如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread())中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。
使用示例
线程上下文类加载器的出现就是为了方便破坏双亲委派。典型应用场景有:
  • SPI 机制:Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
    • OSGi 模块化系统:OSGi(Open Service Gateway Initiative)是一种面向Java的动态模块化系统规范,它允许开发者将应用程序分解为可独立部署、管理的模块(称为 bundle)。每个 bundle 有独立的类加载器,实现类隔离。

    四、常见问题

    如何判断两个 Class 对象是否同一个类?
    • 全限定类名相同
    • 加载这个类的类加载器相同
    即一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()isAssignableFrom()isInstance()以及instanceof关键字等),即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。
    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类的实例化JVM系列:JVM类的初始化之类加载的时机和步骤
    Loading...