当前位置: 首页 > news >正文

【JVM】深入理解 JVM 类加载器

在 Java 技术体系中,类加载器是实现类动态加载的核心组件,它负责将字节码文件加载到 JVM 中并生成对应的 Class 对象。理解类加载器的工作机制,不仅能帮助我们排查类加载相关的问题,更能深入理解 JVM 的内存安全模型。本文将从类加载过程、内置类加载器、双亲委派模型、自定义类加载器到打破委派模型的场景,全面解析类加载器的核心知识。

一、类加载过程回顾

在探讨类加载器之前,我们先简要回顾类加载的完整过程。JVM 将一个类从字节码文件加载到内存并可用,需经历以下步骤:

  1. 加载:通过类的全限定名获取二进制字节流,转换为方法区的运行时数据结构,同时在内存中生成代表该类的Class对象(作为方法区数据的访问入口)。
  2. 连接:包括验证(字节码合法性校验)、准备(静态变量内存分配与默认初始化)、解析(符号引用转换为直接引用)。
  3. 初始化:执行类构造器<clinit>()方法,完成静态变量赋值和静态代码块执行。

其中,加载阶段的核心执行者就是类加载器。它决定了字节流的来源(文件、网络、动态生成等),并直接影响类的唯一性(JVM 通过 “类全限定名 + 类加载器” 唯一标识一个类)。

二、JVM 内置类加载器

JVM 启动时会初始化 3 个核心类加载器,它们构成了类加载的基础层级。在 Java 9 之前,这三个加载器分别是:

1. 启动类加载器(Bootstrap ClassLoader)

  • 实现:由 C++ 编写(非 Java 类),是 JVM 的一部分。
  • 作用:加载 JDK 核心类库,如%JAVA_HOME%/lib目录下的rt.jar(包含java.langjava.util等基础包)、charsets.jar等,以及-Xbootclasspath参数指定的路径。
  • 特点:没有父加载器,在 Java 代码中通常表示为null(因非 Java 实现)。

2. 扩展类加载器(Extension ClassLoader)

  • 实现:Java 类(sun.misc.Launcher$ExtClassLoader),继承自ClassLoader
  • 作用:加载扩展类库,如%JRE_HOME%/lib/ext目录下的 jar 包,以及java.ext.dirs系统变量指定的路径。
  • 特点:父加载器是启动类加载器(逻辑上)。

3. 应用程序类加载器(AppClassLoader)

  • 实现:Java 类(sun.misc.Launcher$AppClassLoader),继承自ClassLoader
  • 作用:加载当前应用 classpath 下的类(包括项目代码、第三方依赖),是开发者接触最多的类加载器。
  • 特点:父加载器是扩展类加载器,可通过ClassLoader.getSystemClassLoader()获取。

Java 9 及之后的变化

Java 9 引入模块系统后,扩展类加载器被平台类加载器(Platform ClassLoader) 取代。除java.base等核心模块由启动类加载器加载外,其他 Java SE 模块均由平台类加载器加载,进一步规范了类加载的边界。

类加载器层级验证

我们可以通过一段简单的代码验证类加载器的层级关系:

public class ClassLoaderDemo {public static void main(String[] args) {ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();while (classLoader != null) {System.out.println(classLoader);classLoader = classLoader.getParent();}System.out.println(classLoader); // 启动类加载器(null)}
}

输出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@53bd815b
null

结果显示:自定义类由 AppClassLoader 加载,其父亲是 ExtClassLoader,最顶层是启动类加载器(null)。

三、双亲委派模型:类加载的黄金法则

类加载器的工作遵循 “双亲委派模型”,这是 JDK 设计的核心机制,用于保证类加载的安全性和唯一性。

1. 什么是双亲委派模型?

双亲委派模型要求:除启动类加载器外,所有类加载器都有一个父加载器;当需要加载一个类时,当前类加载器会先将请求委派给父加载器,只有父加载器无法加载时,才尝试自己加载

这里的 “双亲” 并非指两个父加载器,而是层级委派关系(更准确地说应称为 “单亲委派”)。类加载器之间的父子关系通过组合而非继承实现(ClassLoader类通过parent字段持有父加载器引用)。

2. 执行流程(源码解析)

双亲委派的逻辑集中在ClassLoaderloadClass()方法中,核心步骤如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查类是否已加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 2. 父加载器不为空,委派父加载器加载c = parent.loadClass(name, false);} else {// 3. 父加载器为空(启动类加载器),尝试直接加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父加载器无法加载,继续}if (c == null) {// 4. 父加载器失败,当前加载器调用findClass()加载long t1 = System.nanoTime();c = findClass(name);}}if (resolve) {resolveClass(c); // 解析类}return c;}
}

简单总结流程:

  • 先查缓存(避免重复加载);
  • 再委派父加载器(从顶层到底层);
  • 父加载器失败,自己加载(findClass())。

3. 双亲委派的核心优势

  • 避免类重复加载:同一类由最顶层能加载它的类加载器加载,确保全 JVM 中类的唯一性。
  • 保护核心 API:防止恶意代码篡改核心类(如java.lang.Object)。例如,若自定义一个java.lang.Object,双亲委派会优先让启动类加载器加载 JDK 自带的Object,而非自定义类(且preDefineClass方法会禁止加载java.*开头的类,抛出SecurityException)。

四、自定义类加载器

JVM 允许开发者通过继承ClassLoader类实现自定义类加载器,以满足特殊需求(如加密字节码、从网络加载类等)。

1. 实现方式

自定义类加载器的关键是重写以下方法:

  • findClass(String name):默认空实现,需子类实现 “根据类名获取字节流并生成 Class 对象” 的逻辑。** 不打破双亲委派时,仅需重写此方法 。-loadClass(String name, boolean resolve)**:若需打破双亲委派模型,需重写此方法(覆盖默认的委派逻辑)。

2. 示例:简单自定义类加载器

public class CustomClassLoader extends ClassLoader {private String classPath; // 类加载路径public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 1. 读取字节码文件byte[] data = loadByte(name);// 2. 将字节流转换为Class对象return defineClass(name, data, 0, data.length);} catch (IOException e) {throw new ClassNotFoundException(e.getMessage());}}// 从指定路径加载字节码private byte[] loadByte(String name) throws IOException {name = name.replaceAll("\\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}
}

使用时,若类未被父加载器加载,会调用findClass加载自定义路径下的类。

五、打破双亲委派模型的场景

双亲委派模型虽安全,但并非万能。某些场景下需打破委派(如 SPI 机制、Tomcat 类隔离),常见方式有两种:重写loadClass方法、使用线程上下文类加载器。

1. Tomcat 的类加载器设计

Tomcat 需实现 Web 应用间的类隔离(同一类在不同应用中可独立加载),因此自定义了类加载器层级,打破了双亲委派:
-** WebAppClassLoader :每个 Web 应用一个,优先加载应用WEB-INF/classesWEB-INF/lib下的类,而非先委派父加载器。
-
 核心逻辑 **:重写loadClass方法,先尝试自己加载,若加载失败再委派父加载器(与默认逻辑相反)。

2. 线程上下文类加载器(解决 SPI 问题)

SPI(服务提供者接口)如 JDBC、JNDI 中,接口由 JDK 核心类库提供(BootstrapClassLoader加载),但实现类由第三方提供(需AppClassLoader加载)。按双亲委派,BootstrapClassLoader无法委派给子类加载器,导致无法加载实现类。

解决方案:使用线程上下文类加载器(ThreadContextClassLoader):

  • 线程创建时默认继承父线程的上下文类加载器(通常为AppClassLoader)。
  • SPI 接口的加载器(如BootstrapClassLoader)可通过Thread.currentThread().getContextClassLoader()获取上下文类加载器,委托它加载实现类。

例如,JDBC 加载驱动时:

// DriverManager由BootstrapClassLoader加载
Class.forName("com.mysql.cj.jdbc.Driver"); 
// 实际通过线程上下文类加载器(AppClassLoader)加载mysql驱动

六、总结

类加载器是 JVM 实现动态类加载的核心,其设计体现了 “委派” 与 “隔离” 的思想:

  • 双亲委派模型通过层级委派保证了类的唯一性和核心 API 的安全;
  • 自定义类加载器可满足特殊场景(加密、网络加载等);
  • 打破委派模型的机制(如 Tomcat 类加载器、线程上下文类加载器)则解决了灵活性与层级限制的矛盾。

理解类加载器不仅有助于排查ClassNotFoundException、类冲突等问题,更能深入掌握 JVM 的内存管理与安全模型,是 Java 高级开发的必备知识。

http://www.lryc.cn/news/591039.html

相关文章:

  • MySQL如何解决事务并发的幻读问题
  • JVM 内存分配与垃圾回收策略
  • macOS 字体管理全攻略:如何查看已安装字体及常见字体格式区
  • 网络编程7.17
  • JAVA中的Collection集合及ArrayList,LinkedLIst,HashSet,TreeSet和其它实现类的常用方法
  • MyBatis延迟加载(Lazy Loading)之“关联查询”深度解析与实践
  • 【44】MFC入门到精通——MFC 通过Button按钮添加控件变量实现:按下 按钮变色 (比如开关 打开关闭状态) MFC更改button控颜色
  • 数据结构-2(链表)
  • 基于STM32闭环步进电机控制系统设计说明
  • Leaflet地图交互:实现图形与点的同时高亮效果
  • PyTorch生成式人工智能(18)——循环神经网络详解与实现
  • 【Linux基础知识系列】第五十一篇 - Linux文件命名规范与格式
  • Mac 安装及使用sdkman指南
  • Java 大视界 -- Java 大数据在智能交通智能公交站台乘客流量预测与服务优化中的应用(349)
  • Flask+LayUI开发手记(十一):选项集合的数据库扩展类
  • Java 集合框架详解:Collection 接口全解析,从基础到实战
  • 【LeetCode 热题 100】108. 将有序数组转换为二叉搜索树
  • 【Redis 】看门狗:分布式锁的自动续期
  • 如何用Kaggle免费GPU
  • [yotroy.cool] Git 历史迁移笔记:将 Git 项目嵌入另一个仓库子目录中(保留提交记录)
  • 语雀编辑器内双击回车插入当前时间js脚本
  • 【WRFDA第六期】WRFDA 输出文件详述
  • R语言基础| 基本图形绘制(条形图、堆积图、分组图、填充条形图、均值条形图)
  • Spring AI之Prompt开发
  • Web攻防-PHP反序列化Phar文件类CLI框架类PHPGGC生成器TPYiiLaravel
  • Cursor开发步骤
  • 【C++指南】C++ list容器完全解读(四):反向迭代器的巧妙实现
  • 113:路径总和 II
  • Java学习--JVM(2)
  • 基于FPGA的IIC控制EEPROM读写(2)