什么是类加载器(ClassLoader)?
类加载器(ClassLoader) 是 Java 虚拟机(JVM)加载字节码文件(.class)到内存,并转换成 Class 对象的组件。
1、 为什么需要类加载器?
Java 语言的特点是动态加载:
- 代码编译后变成 .class 文件,但 .class 文件本身不能直接运行,必须由 类加载器 加载到 JVM 内存中,才能被使用。
- 类加载器让 Java 可以在运行时动态加载类,比如:
- 反射(Reflection)
- 热部署(Hot Deployment)
- 插件机制(Plugin System)
2、 类加载器的作用
类加载器的主要功能是:
- 加载类(Load Class):从磁盘、网络、Jar 包等地方加载 .class 文件到 JVM 内存。
- 连接类(Link Class):
(1) 验证(Verify):检查字节码是否符合 JVM 规范,防止恶意代码执行。
(2) 准备(Prepare):为类的静态变量分配内存,并初始化默认值。
(3) 解析(Resolve):将符号引用转换为直接引用(如方法、字段)。 - 初始化类(Initialize Class):
执行类的 静态代码块(static {}) 和 静态变量初始化。
3、Java 的类加载器层次结构
Java 提供了 层次化的类加载器(资源隔离,避免重复加载),主要有以下几种:
类加载器 | 作用 | 典型加载的类 |
---|---|---|
Bootstrap ClassLoader(引导类加载器) | 加载 Java 核心类 | java.lang.String, java.util.List |
ExtClassLoader(扩展类加载器)/ PlatformClassLoader(Java 9+) | 加载 lib/ext/ 目录下的类 | javax.crypto.Cipher |
AppClassLoader(应用类加载器) | 加载 classpath 下的类 | com.example.Main |
自定义 ClassLoader(用户自定义) | 加载自定义路径的类 | 插件、热部署类 |
💡 层次关系(双亲委派模型 Parent Delegation Model)
Bootstrap ClassLoader (引导类加载器)
↑
ExtClassLoader / PlatformClassLoader(扩展类/平台类加载器)
↑
AppClassLoader(应用类加载器)
↑
自定义 ClassLoader(用户自定义的类加载器)
4、Java 如何使用类加载器?
(1)获取类加载器
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取当前类的类加载器(AppClassLoader)
ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("AppClassLoader: " + appClassLoader);
// 获取当前类加载器的父类加载器(ExtClassLoader / PlatformClassLoader)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("ExtClassLoader: " + extClassLoader);
// 获取 ExtClassLoader 的父类加载器(BootstrapClassLoader,C++ 实现,返回 null)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("BootstrapClassLoader: " + bootstrapClassLoader);
}
}
示例输出(Java 8):
AppClassLoader: sun.misc.Launcher$AppClassLoader@<hash>
ExtClassLoader: sun.misc.Launcher$ExtClassLoader@<hash>
BootstrapClassLoader: null
(2)使用 Class.forName() 反射加载类
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("类加载器:" + clazz.getClassLoader()); // 输出 null(由 Bootstrap 加载)
(3)使用 ClassLoader.loadClass() 手动加载类
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> myClass = classLoader.loadClass("com.example.MyClass");
System.out.println("MyClass 加载器:" + myClass.getClassLoader());
5、自定义类加载器(扩展 ClassLoader)
有时,我们需要自定义类加载器,比如:
- 从网络或数据库加载类
- 动态代理
- 热部署(Hot Deployment)
示例:自定义 ClassLoader
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String name) {
String fileName = classPath + name.replace(".", "/") + ".class";
try (InputStream is = new FileInputStream(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("加载类失败", e);
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/path/to/classes/");
Class<?> clazz = myClassLoader.loadClass("com.example.HelloWorld");
System.out.println("自定义类加载器加载的类:" + clazz.getName());
}
}
示例解析:
- 继承 ClassLoader 并重写 findClass() 方法。
- 从指定路径加载 .class 文件的二进制数据,转换为 Class 对象。
- 通过 defineClass() 方法将字节码转换为 Java 类。
什么是双亲委派机制(Parent Delegation Model)?
双亲委派机制是 Java 类加载器的一种工作方式。它规定:
- 当某个类加载器加载一个类时,它会先请求它的 父类加载器 尝试加载。
- 如果父类加载器找不到这个类,它才会自己尝试加载。
目的: - 防止核心类库被篡改(如 java.lang.String 不能被用户定义的类替换)。
- 提高加载效率,避免重复加载同一个类。
1、双亲委派的类加载流程
假设要加载 com.example.MyClass,流程如下:
- AppClassLoader 先检查自己是否已加载过 com.example.MyClass。
- 如果没有,它先委托父加载器 ExtClassLoader 进行加载。
- ExtClassLoader 再委托BootstrapClassLoader 尝试加载。
- BootstrapClassLoader 只加载核心类库(如 java.lang.*),找不到 com.example.MyClass,返回 null。
- ExtClassLoader 也找不到 com.example.MyClass,返回 null。
- AppClassLoader 发现父类加载器都无法加载,自己来加载 com.example.MyClass。
2、验证双亲委派机制
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("AppClassLoader: " + appClassLoader);
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("ExtClassLoader: " + extClassLoader);
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("BootstrapClassLoader: " + bootstrapClassLoader); // null
// 验证 java 核心类由 BootstrapClassLoader 加载
System.out.println("String 类加载器:" + String.class.getClassLoader()); // null
// 验证 AppClassLoader 加载自定义类
System.out.println("当前类的类加载器:" + ClassLoaderTest.class.getClassLoader());
}
}
示例输出(Java 8):
AppClassLoader: sun.misc.Launcher$AppClassLoader@<hash>
ExtClassLoader: sun.misc.Launcher$ExtClassLoader@<hash>
BootstrapClassLoader: null
String 类加载器:null
当前类的类加载器:sun.misc.Launcher$AppClassLoader@<hash>
- String 由 BootstrapClassLoader 加载(返回 null)。
- 自定义类由 AppClassLoader 加载。
3、三个类加载器之间的关系
不是继承关系,而是层级关系(父子关系)
- 父类加载器不一定是 Java 类的父类,而是通过 parent 引用(组合的方式) 来建立层级关系。如下为ClassLoader中父加载器的定义:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}
- 例如:AppClassLoader 的 父类加载器 是 ExtClassLoader,但 AppClassLoader 和 ExtClassLoader 并没有继承关系,只是它的父加载器。
ClassLoader的继承体系
java.lang.Object
└── java.lang.ClassLoader
├── java.net.URLClassLoader (Java 8 及之前)
│ ├── sun.misc.Launcher$AppClassLoader
│ ├── sun.misc.Launcher$ExtClassLoader
├── jdk.internal.loader.BuiltinClassLoader (Java 9+)
├── jdk.internal.loader.ClassLoaders$AppClassLoader
├── jdk.internal.loader.ClassLoaders$PlatformClassLoader
- AppClassLoader 和 ExtClassLoader 继承自 URLClassLoader(Java 8)。
- PlatformClassLoader 和 AppClassLoader 继承自 BuiltinClassLoader(Java 9)。
- BootstrapClassLoader 不是 Java 类,没有继承 ClassLoader。
4、什么时候需要打破双亲委派?
通常情况下,不建议破坏双亲委派,除非有特殊需求:
- Tomcat / OSGi 需要自定义类加载器以实现热加载和模块化。
- Spring Boot 在 spring-boot-devtools 里使用了自定义类加载器,支持类热加载。
- JDBC 驱动 需要自定义 Thread Context ClassLoader,否则 BootstrapClassLoader 无法加载第三方数据库驱动
5、如何破坏双亲委派机制?
双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
loadClass()、findClass()、defineClass()区别
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
- loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
- findClass() 根据名称或位置加载.class字节码
- definclass() 把字节码转化为Class
示例:
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 破坏双亲委派机制:先尝试自己加载
if (name.startsWith("java.") || name.startsWith("javax.")) {
return super.loadClass(name); // 保持 JDK 核心类不被篡改
}
try {
byte[] data = loadClassData(name);
Class<?> clazz = defineClass(name, data, 0, data.length);
if (resolve) {
resolveClass(clazz);
}
return clazz;
} catch (IOException e) {
return super.loadClass(name); // 加载失败时,交给父类加载器
}
}
private byte[] loadClassData(String name) throws IOException {
String fileName = classPath + name.replace(".", "/") + ".class";
try (InputStream is = new FileInputStream(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/path/to/classes/");
Class<?> clazz = myClassLoader.loadClass("com.example.HelloWorld");
System.out.println("自定义类加载器加载的类:" + clazz.getName());
System.out.println("类的类加载器:" + clazz.getClassLoader());
}
}