Java类加载机制
Java 的类加载机制是 JVM 将类的字节码文件(.class)加载到内存,并对其进行验证、准备、解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。它是 Java 实现跨平台、动态扩展(如 SPI、热部署)的核心基础之一。
类加载的完整生命周期
一个 Java 类从被加载到 JVM 内存中,到最终被卸载,其生命周期包括加载、验证、准备、解析、初始化、使用、卸载7 个阶段。其中加载、验证、准备、初始化、卸载这 5 个阶段的顺序是确定的,而解析阶段则可能在初始化阶段之后(为了支持动态绑定,即晚期绑定)。
graph LR
A[加载]
subgraph 连接
B[验证]
C[准备]
D[解析]
end
E[初始化]
F[使用]
G[卸载]
A-->B-->C-.->D-.->E-->F-->G
类加载ClassLoader
类加载ClassLoader介绍
类加载器是实现 “加载” 阶段的核心组件,负责获取类的二进制字节流。JVM 规范将类加载器分为启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader),以及用户自定义的自定义类加载器(Custom ClassLoader)。JVM会自顶向下尝试加载类
graph BT
subgraph JVM
A[启动类加载器 BootstrapClassLoader]
end
B[扩展类加载器 ExtensionClassLoader]
C[应用程序类加载器 AppClassLoader]
D[自定义类加载器 CustomClassLoader]
E[自定义类加载器 CustomClassLoader]
F[定义类加载器 CustomClassLoader]
B-->A
C-->B
D-->C
E-->C
F-->D
除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类加载器的父类加载器是 BootstrapClassLoader 。
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 等非核心模块);
通过如下代码可以看出:
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 引入的模块系统)的类加载机制也兼容了双亲委派模型的核心逻辑,模块的加载优先级仍遵循 “系统模块优先于用户模块” 的原则,与双亲委派的安全设计一脉相承。
双亲委派模型的实现
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;
}
}
打破双亲委派模型
为什么要打破双亲委托模型?
双亲委派模型决定了"自下而上加载,自上而下委派"的类加载顺序。
graph BT
A[BootstrapClassLoader]
B[ExtClassLoader/PlatformClassLoader]
C[AppClassLoader]
X[java核心库]
Y[java扩展库]
Z[用户程序]
C-->B-->A
Z-->Y-->X
对于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,但是版本不同。
graph BT
A[App]
B[UserService]
C[Calculator:1.0]
D[AdminService]
E[Calculator:1.0]
E-->D-->A
C-->B-->A
Calculator 1.0提供了两个整数加法,Calculator 2.0提供了三个整数加法
// 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目录,打包命令如下:
jar cfv xxx.jar org/
最后目录层级如下:
|---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,实现类加载隔离
|---BootstrapClassLoader
|---PlatformClassLoader
|---AppClassLoader
|---App.class
|---URLClassLoader1
|---calculator-1.0
|---admin-1.0
|---URLClassLoader2
|---calculator-2.0
|---user-1.0
样例如下:
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;
}
}
最后输出
AdminService doTask3
UserService doTask6