在 Java 世界里,类加载器是保证 Java 程序正常运行的 “幕后英雄”,它负责把.class
文件加载到 JVM 中,变成可以使用的 Class 对象。今天我们就来好好聊聊类加载器的那些事儿,从分类到核心机制,再到实际应用~
目录
一、类加载器的分类
类加载器主要分两类,很好理解:
- 虚拟机底层实现的类加载器:这类加载器的源代码在 Java 虚拟机的源码里,实现语言和虚拟机底层语言一致。比如 Hotspot 虚拟机用的是 C++。它的作用是加载程序运行时的基础类,像
java.lang.String
这种核心类,得靠它来保证加载的可靠性,毕竟这些类是 Java 程序运行的基石。 - Java 代码实现的类加载器:JDK 里默认提供了好几种,能处理不同渠道的类加载需求,而且程序员还能根据自己的需求自定义。所有 Java 里实现的类加载器都得继承
ClassLoader
这个抽象类。
在 JDK8 及之前的版本中,Java 代码实现的类加载器又细分为以下几种:
- 启动类加载器(Bootstrap ClassLoader):属于虚拟机底层实现的范畴,加载 Java 中最核心的类,像
java.lang
包下的类,这些类是 Java 运行的核心支撑。 - 扩展类加载器(Extension ClassLoader):负责加载 Java 的扩展类,允许扩展 Java 中比较通用的类,拓展 Java 的功能。
- 应用程序类加载器(Application ClassLoader):也叫系统类加载器,主要加载应用使用的类,也就是我们自己写的代码以及依赖的第三方库中的类。
二、核心机制:双亲委派
(一)什么是双亲委派机制
双亲委派机制是类加载器的核心运行规则。当一个类加载器收到加载类的任务时,它不会自己先尝试加载,而是自底向上(从应用程序类加载器到扩展类加载器,再到启动类加载器)查找这个类是否已经被加载过;如果都没加载过,再由顶向下尝试加载。
(二)举个例子理解
比如要加载一个com.example.MyClass
类,应用程序类加载器会先问扩展类加载器:“你加载过这个类吗?” 扩展类加载器又去问启动类加载器。启动类加载器一看,自己负责的核心类里没有这个,就回复扩展类加载器没加载过。然后扩展类加载器也说自己没加载过,这时候应用程序类加载器才会尝试去加载com.example.MyClass
类。
三)双亲委派的好处
- 避免类的重复加载:如果多个类加载器都能加载同一个类,双亲委派能保证只有一个类加载器去加载,避免重复。
- 保证核心类的安全性:像
java.lang.String
这样的核心类,只能由启动类加载器加载。如果我们自己写一个java.lang.String
类,按照双亲委派机制,应用程序类加载器会先委托启动类加载器,而启动类加载器已经加载了核心的String
类,就不会再加载我们自己写的,这样就防止了核心类被篡改,保证了 Java 运行的安全性。
三、打破双亲委派机制的方式
虽然双亲委派机制很有用,但在一些场景下需要打破它,主要有以下几种方式,这里重点讲自定义类加载器和线程上下文类加载器。
(一)自定义类加载器
我们可以自定义类加载器,并重写loadClass
方法,把双亲委派机制的代码去掉,这样就能自己控制类的加载逻辑。
应用场景:Tomcat 就通过这种方式实现应用之间的类隔离。因为 Tomcat 要部署多个 Web 应用,每个应用都有自己的类库,自定义类加载器可以让不同应用的类相互隔离,避免类冲突。
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
// 重写findClass方法,自定义类的查找和加载逻辑
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
// 加载类的字节码数据
private byte[] loadClassData(String name) throws IOException {
name = name.replace('.', '/') + ".class";
FileInputStream fis = new FileInputStream(classPath + "/" + name);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != -1) {
bos.write(b);
}
fis.close();
return bos.toByteArray();
}
// 这里重写loadClass,打破双亲委派(简化版,实际可根据需求调整)
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
// 这里不进行双亲委派,直接自己找
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
在这个例子中,MyClassLoader
重写了loadClass
方法,不再向上委托父类加载器,而是自己通过findClass
方法去加载类。
(二)线程上下文类加载器
线程上下文类加载器是每个线程都有的一个类加载器。我们可以利用它来加载类,像 JDBC、JNDI 等技术就用到了这种方式。
为什么需要它
例:JDBC 的核心类(如DriverManager
)位于rt.jar
包中,由 启动类加载器(Bootstrap ClassLoader)加载。而具体的数据库驱动(如 MySQL 的com.mysql.cj.jdbc.Driver
)是第三方库,位于应用的类路径下,理论上应由应用程序类加载器(Application ClassLoader) 加载。
按照双亲委派机制,DriverManager
(启动类加载器加载)要加载驱动类时,会委托父类加载器(但启动类加载器没有父类了),然后自己尝试加载 —— 但启动类加载器只能加载rt.jar
里的类,根本找不到应用类路径下的驱动,这就产生了矛盾。
为了解决这个矛盾,JDBC 引入了线程上下文类加载器(Thread Context ClassLoader):
- 启动类加载器加载
DriverManager
:这一步完全符合双亲委派。 DriverManager
初始化时,通过 SPI 机制加载驱动:DriverManager
会用ServiceLoader
去加载驱动类。而ServiceLoader
的关键操作是:获取当前线程的上下文类加载器(默认是应用程序类加载器),并委托它去加载驱动类。
JDBC 的这种设计,本质是为SPI(Service Provider Interface)机制服务的:
- SPI 要求 “接口由核心类加载器加载,实现由应用类加载器加载”,而双亲委派本身无法直接支持这种 “跨类加载器” 的协作。
- 线程上下文类加载器相当于在类加载器的层级间开了一个 “后门”,让高层级的类加载器能委托低层级的类加载器,从而实现了 “核心接口” 与 “第三方实现” 的解耦加载。
import java.sql.DriverManager; import java.sql.SQLException; import java.util.Enumeration; public class JdbcClassLoaderDemo { public static void main(String[] args) throws SQLException { // 1. 查看当前线程的上下文类加载器(默认是应用程序类加载器) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); System.out.println("线程上下文类加载器: " + contextClassLoader); // 2. 查看DriverManager的类加载器(启动类加载器,输出为null) ClassLoader driverManagerClassLoader = DriverManager.class.getClassLoader(); System.out.println("DriverManager的类加载器: " + driverManagerClassLoader); // 3. 加载并注册驱动(实际开发中无需显式调用,DriverManager会自动扫描) try { // 这里模拟DriverManager的加载过程 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); return; } // 4. 查看已注册的驱动及其类加载器 Enumeration<java.sql.Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { java.sql.Driver driver = drivers.nextElement(); System.out.println("\n发现驱动: " + driver.getClass().getName()); System.out.println("驱动的类加载器: " + driver.getClass().getClassLoader()); } // 5. 演示线程上下文类加载器的作用 System.out.println("\n--- 线程上下文类加载器的作用 ---"); System.out.println("DriverManager(启动类加载器)通过线程上下文类加载器(" + contextClassLoader + ")加载了驱动类"); } }
(三)Osgi 框架的类加载器
历史上 Osgi 框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载,不过相比前两种,在日常开发中接触相对少一些。
四、JDK9 之后的类加载器区别
JDK9 引入了模块系统(JPMS),类加载器的架构也有了一些变化:
- 模块层的类加载:类是属于模块的,类加载器需要考虑模块的可见性等规则。
- 扩展类加载器的变化:扩展类加载器(Extension ClassLoader)被替换成了平台类加载器(Platform ClassLoader),它负责加载 Java 平台模块中的类,功能上和之前的扩展类加载器有相似之处,但更贴合模块系统的设计。
- 模块化影响:类的加载不仅要考虑类加载器的委托关系,还要考虑模块之间的依赖和可见性,使得类加载的逻辑更加精细和复杂,以适应模块化的 Java 系统。
总结
类加载器是 JVM 中非常关键的部分,双亲委派机制保证了类加载的秩序和核心类的安全,但在一些特殊场景下,我们也需要通过自定义类加载器、线程上下文类加载器等方式打破它。而 JDK9 之后,随着模块系统的引入,类加载器的机制也朝着更精细化的方向发展,以适应 Java 生态的演进。希望这篇文章能让你对类加载器有更清晰的认识~