Java类加载机制
Java 的类加载机制是 JVM 将类的字节码文件(.class)加载到内存,并对其进行验证、准备、解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。它是 Java 实现跨平台、动态扩展(如 SPI、热部署)的核心基础之一。
类加载的完整生命周期
一个 Java 类从被加载到 JVM 内存中,到最终被卸载,其生命周期包括加载、验证、准备、解析、初始化、使用、卸载 7 个阶段。其中加载、验证、准备、初始化、卸载 这 5 个阶段的顺序是确定的,而解析阶段则可能在初始化阶段之后(为了支持动态绑定,即晚期绑定)。
连接 验证 准备 解析 加载 初始化 使用 卸载
类加载ClassLoader
类加载ClassLoader介绍
类加载器是实现 "加载" 阶段的核心组件,负责获取类的二进制字节流。JVM 规范将类加载器分为启动类加载器(Bootstrap ClassLoader) 、扩展类加载器(Extension ClassLoader) 、应用程序类加载器(Application ClassLoader) ,以及用户自定义的自定义类加载器(Custom ClassLoader)。JVM会自顶向下尝试加载类
JVM 启动类加载器 BootstrapClassLoader 扩展类加载器 ExtensionClassLoader 应用程序类加载器 AppClassLoader 自定义类加载器 CustomClassLoader 自定义类加载器 CustomClassLoader 定义类加载器 CustomClassLoader
除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类加载器的父类加载器是 BootstrapClassLoader 。
java
public abstract class ClassLoader {
// 父加载器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
...
}
其中jdk8及之前版本存在ExtensionClassLoader,用于加载 $JAVA_HOME/jre/lib/ext 目录下的扩展 jar 包;Java 9 引入模块化系统(JPMS)后,JDK 架构发生根本性调整,导致 ExtensionClassLoader 被废弃。原 ExtClassLoader 的职责被 jdk.internal.loader.PlatformClassLoader 接管,但 PlatformClassLoader 不再基于 ext 目录,而是加载 Java SE 平台的 "系统模块"(如 java.desktop、java.sql 等非核心模块);
通过如下代码可以看出:
java
public class App
{
public static void main( String[] args )
{
// 输出为null,默认为jdk内置BootstrapClassLoader
System.out.println(String.class.getClassLoader());
// 非核心库由PlatformClassLoader/ExtClassLoader加载
System.out.println(Driver.class.getClassLoader());
// 用户应用由AppClassLoader加载
System.out.println(App.class.getClassLoader());
}
}
双亲委派模型
双亲委派模型介绍
ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。JVM中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。
从上面的介绍可以看出:
ClassLoader类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
双亲委派模型的作用
双亲委派模型是 Java 类加载机制的核心设计之一,其意义主要体现在安全性、唯一性、规范性等多个维度,是 JVM 实现类加载有序性和可靠性的关键保障,具体可分为以下几个核心方面:
-
避免类的重复加载,保证类的唯一性
JVM 判定两个类是否为 "同一个类" 的标准是:类的全限定名相同 + 加载该类的类加载器相同。双亲委派模型通过 "先委派父类加载器加载" 的规则,确保同一个类只会被其最顶层的可用类加载器加载一次,而非被多个类加载器重复加载。
例如,应用程序类加载器收到加载
java.lang.String的请求时,会先委派给扩展类加载器,最终由启动类加载器加载该类。后续任何类加载器再请求加载java.lang.String时,都会因父类加载器已加载该类而直接复用,避免了内存中出现多个String类的实例,减少了内存开销,也保证了类的逻辑一致性。 -
保护核心类库的安全,防止 "类篡改" 攻击
-
双亲委派模型将 JVM 核心类库(如
java.lang、java.util等包下的类)的加载权完全交给启动类加载器(Bootstrap ClassLoader),这是对 Java 核心 API 的关键安全防护:- 若没有双亲委派模型,用户可自定义一个
java.lang.String类,并通过应用程序类加载器加载,从而篡改核心类的逻辑(如修改String的equals方法),引发严重的安全问题。 - 而双亲委派模型下,用户自定义的
java.lang.String类会因 "父类加载器(启动类加载器)已加载核心String类" 而无法被加载,从根本上杜绝了恶意代码替换核心类库的风险,保证了 Java 运行时环境的基础安全。
- 若没有双亲委派模型,用户可自定义一个
-
规范类加载的层级关系,实现类加载的有序性
双亲委派模型为类加载器定义了清晰的层级结构(启动类加载器 → 扩展类加载器 → 应用程序类加载器 → 自定义类加载器),并规定了 "自上而下委派、自下而上加载" 的执行顺序,使得类加载过程具有明确的规范性和可预测性:
- 不同类加载器的职责边界被明确划分:启动类加载器负责核心类库,扩展类加载器负责 JVM 扩展类,应用程序类加载器负责用户业务类,自定义类加载器负责特殊需求(如网络加载、加密类加载)。
- 这种层级划分避免了类加载器之间的职责混乱,简化了类加载机制的设计与实现,也便于开发者理解和扩展自定义类加载器(只需遵循委派规则,即可与系统类加载器协同工作)。
-
为模块化类加载奠定基础
双亲委派模型的层级委派思想,为 Java 后续的模块化发展(如 JPMS/Jigsaw、OSGI 的模块化类加载)提供了基础设计思路:
- 尽管 OSGI 等模块化框架突破了双亲委派的严格顺序(采用 "平级委派 + 按需加载"),但依然借鉴了 "先委托高层 / 公共类加载器加载,再自行加载" 的核心思想,以实现模块间的类隔离与共享。
- JPMS(Java 9 引入的模块系统)的类加载机制也兼容了双亲委派模型的核心逻辑,模块的加载优先级仍遵循 "系统模块优先于用户模块" 的原则,与双亲委派的安全设计一脉相承。
双亲委派模型的实现
java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载
Class<?> c = findLoadedClass(name);
// 如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果有父类加载器,委派父类加载器加载类
c = parent.loadClass(name, false);
} else {
// 如果没有父类加载器,使用JVM中BootstrapClassLoader加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果仍未加载到类,使用findClass找到类
long t1 = System.nanoTime();
c = findClass(name);
//...记录状态
}
}
if (resolve) {
// 对类进行连接操作
resolveClass(c);
}
return c;
}
}
打破双亲委派模型
为什么要打破双亲委托模型?
双亲委派模型决定了"自下而上加载,自上而下委派"的类加载顺序。
BootstrapClassLoader ExtClassLoader/PlatformClassLoader AppClassLoader java核心库 java扩展库 用户程序
对于Web 容器(Tomcat、Jetty)、模块化容器(OSGi)的核心诉求是类隔离 (不同应用 / 模块使用不同版本的类,互不干扰),而双亲委托的 "父优先" 会导致:父加载器(如AppClassLoader)加载的类会被所有子应用共享,无法隔离(比如两个 Webapp 依赖不同版本的 Spring,父加载器加载高版本后,低版本应用会冲突)。
如何打破双亲委派模型
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
因为loadClass() 方法已经明确了加载类的流程,先委托给父类加载器加载,最终给BootstrapClassLoader去加载,如果都加载不到,才会调用findClass() 方法。只需要重写loadClass() 方法的加载流程即可
自定义类加载器作用
为什么要自定义类加载器
JVM 提供Thread.setContextClassLoader(),允许用户自定义当前线程的类加载器从而实现打破双亲委派机制,优先由用户设定的类加载器进行加载,当加载失败时才尝试使用父加载器加载,从而达到类隔离的场景。
如何自定义类加载器
URLClassLoader 是 Java 提供的标准自定义类加载器实现,核心作用是通过 URL 路径 (文件、JAR、网络地址等)加载类,突破默认 classpath 限制;默认遵循双亲委托模型,但可通过重写方法打破。基本上自定义的类加载器都继承自URLClassLoader。
有如下诉求,App分别调用UserService和AdminService,他们都依赖Calculator,但是版本不同。
App UserService Calculator:1.0 AdminService Calculator:1.0
Calculator 1.0提供了两个整数加法,Calculator 2.0提供了三个整数加法
java
// 1.0服务两个整数相加
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// 2.0服务三个整数相加
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// admin服务调用Calculator 1.0服务
public class AdminService {
private final Calculator calculator = new Calculator();
public AdminService(Calculator calculator) {
}
public void doTask() {
System.out.println("AdminService doTask" + this.calculator.add(1, 2));
}
}
// user服务调用Calculator 2.0服务
public class UserService {
private final Calculator calculator = new Calculator();
public UserService(Calculator calculator) {
}
public void doTask() {
System.out.println("UserService doTask" + this.calculator.add(1, 2, 3));
}
}
可以使用IDEA将上述文件编译为class后,分别按package的目录层级打成4个jar包,放在src/main/resources目录,打包命令如下:
shell
jar cfv xxx.jar org/
最后目录层级如下:
shell
|---src
|---main
|---java
|---xxx.xxx.xx
|---App.java
|---resources
|---admin
|---1.0
|---admin.jar
|---user
|---1.0
|---user.jar
|---calculator
|---1.0
|---calculator.jar
|---2.0
|---calculator.jar
在App.java中模拟扫描jar包并加载的过程,其中calculator1.0和admin1.0使用classLoader1,calculator2.0和user1.0使用classLoader2,实现类加载隔离
shell
|---BootstrapClassLoader
|---PlatformClassLoader
|---AppClassLoader
|---App.class
|---URLClassLoader1
|---calculator-1.0
|---admin-1.0
|---URLClassLoader2
|---calculator-2.0
|---user-1.0
样例如下:
java
public class App {
public static void main(String[] args) throws Throwable {
URL calculatorV1Url = App.class.getClassLoader().getResource("calculator/1.0/calculator.jar");
URL adminServiceUrl = App.class.getClassLoader().getResource("admin/1.0/admin.jar");
URL calculatorV2Url = App.class.getClassLoader().getResource("calculator/2.0/calculator.jar");
URL userServiceUrl = App.class.getClassLoader().getResource("user/1.0/user.jar");
try (URLClassLoader classLoader1 = new URLClassLoader(new URL[]{calculatorV1Url, adminServiceUrl})) {
Object calculator1 = loadClass(classLoader1, "org.numb.java.base.classloader.Calculator");
Object adminService = loadClass(classLoader1, "org.numb.java.base.classloader.AdminService", calculator1);
adminService.getClass().getMethod("doTask").invoke(adminService);
}
try (URLClassLoader classLoader2 = new URLClassLoader(new URL[]{calculatorV2Url, userServiceUrl})) {
Object calculator2 = loadClass(classLoader2, "org.numb.java.base.classloader.Calculator");
Object userService = loadClass(classLoader2, "org.numb.java.base.classloader.UserService", calculator2);
userService.getClass().getMethod("doTask").invoke(userService);
}
}
private static Object loadClass(URLClassLoader urlClassLoader, String className, Object... args) throws Throwable {
Class<?> aClass = urlClassLoader.loadClass(className);
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
if (args == null || args.length == 0) {
if (declaredConstructor.getParameterCount() == 0) {
return declaredConstructor.newInstance();
}
}
if (args != null && args.length == declaredConstructor.getParameterCount()) {
return declaredConstructor.newInstance(args);
}
}
return null;
}
}
最后输出
shell
AdminService doTask3
UserService doTask6