【JVM虚拟机】类加载机制:类加载器、双亲委派模型、好处、破坏双亲委派的场景(附《思维导图》+《面试高频考点清单》)
文章目录

JVM虚拟机类加载机制:系统性知识体系总结
一、类加载机制整体概述
JVM类加载机制是指虚拟机将Class文件中的二进制数据加载到内存,经过验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。
核心特点:
- 动态加载:Java程序在运行时才动态加载所需的类,而非一次性加载所有类
- 按需加载:只有当类被主动使用时才会触发初始化
- 类型安全:通过类加载机制保证Java语言的类型安全特性
二、类加载的完整生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括7个阶段:
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段在某些情况下可以在初始化之后开始(支持Java的动态绑定特性)。
1. 加载阶段
-
主要任务:
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
二进制字节流来源:
- 本地文件系统(最常见)
- 网络(如Applet)
- 压缩包(JAR、WAR)
- 运行时动态生成(如ASM框架、动态代理)
- 数据库读取
- 加密文件解密后获取
2. 验证阶段
- 目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 主要验证内容:
- 文件格式验证:验证字节流是否符合Class文件格式规范
- 元数据验证:对字节码描述的信息进行语义分析,确保符合Java语言规范
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证:确保解析动作能正确执行
3. 准备阶段
- 任务:为类的静态变量分配内存并设置默认初始值
- 注意点:
- 仅为静态变量分配内存,实例变量在对象实例化时才分配
- 这里的初始值是数据类型的默认值(如
int为0,boolean为false,引用类型为null) - 被
final修饰的静态常量会在准备阶段直接赋值为代码中指定的值
4. 解析阶段
- 任务:将常量池内的符号引用替换为直接引用
- 符号引用:以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关
- 直接引用:可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄,与虚拟机实现的内存布局直接相关
5. 初始化阶段
-
触发条件(主动使用):
- 创建类的实例(使用
new关键字) - 访问类的静态变量或静态方法
- 调用
java.lang.reflect包的方法对类进行反射调用 - 初始化子类时,父类会先被初始化
- 虚拟机启动时,被指定为启动类的类(包含
main()方法的类) - JDK 1.7及以上的动态语言支持相关场景
- 创建类的实例(使用
-
主要任务:执行类构造器
<clinit>()方法<clinit>()方法由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并产生- 虚拟机会保证子类的
<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕 - 虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步
三、类加载器详解
类加载器负责实现加载阶段中"通过类的全限定名获取定义此类的二进制字节流"的动作。
1. 类加载器的层次结构(JDK 8及之前)
JVM提供了三种类加载器,形成了双亲委派的层次结构:
| 类加载器 | 负责加载的类 | 加载路径 | 实现方式 |
|---|---|---|---|
| 启动类加载器 (Bootstrap ClassLoader) |
Java核心类库 | JAVA_HOME/jre/lib |
C++实现,虚拟机的一部分 |
| 扩展类加载器 (Extension ClassLoader) |
Java扩展类库 | JAVA_HOME/jre/lib/ext |
Java实现,继承自ClassLoader |
| 应用程序类加载器 (Application ClassLoader) |
用户应用程序的类 | 应用程序的classpath | Java实现,继承自URLClassLoader |
| 自定义类加载器 | 用户自定义的类 | 用户指定的路径 | 继承自ClassLoader或其子类 |
2. JDK 9及以上的模块化类加载器
JDK 9引入了模块系统,类加载器结构发生了重大变化:
- 启动类加载器仍然由C++实现
- 扩展类加载器被平台类加载器(Platform ClassLoader)取代
- 应用程序类加载器保持不变
- 每个模块都有自己的类加载器
- 双亲委派模型仍然存在,但实现方式有所调整
3. 类加载器的核心方法
loadClass(String name):加载指定全限定名的类,实现双亲委派逻辑findClass(String name):查找类,自定义类加载器通常重写此方法defineClass(String name, byte[] b, int off, int len):将字节数组转换为Class对象resolveClass(Class<?> c):链接指定的类
四、双亲委派模型
1. 核心原理
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的"双亲"指的是类加载器的父加载器,而非继承关系。
工作流程:
- 当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类
- 而是将这个请求委派给父类加载器去完成
- 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器
- 只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
应用程序类加载器 → 扩展类加载器 → 启动类加载器
↓ ↓ ↓
无法加载 无法加载 无法加载
↓ ↓ ↓
自己加载 自己加载 抛出异常
2. 双亲委派模型的实现代码(核心逻辑)
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 {
// 父类加载器为null,说明是启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载时抛出异常
}
if (c == null) {
// 父类加载器无法加载时,调用自己的findClass方法加载
long t1 = System.nanoTime();
c = findClass(name);
// 记录类加载统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
五、双亲委派模型的好处
1. 保证Java核心类库的安全性
- 防止核心类库被恶意篡改
- 例如:用户无法自定义
java.lang.String类来覆盖JDK自带的String类 - 因为任何自定义的类加载器都会先委派给启动类加载器加载,启动类加载器会加载JDK自带的String类
2. 保证类的唯一性
- 同一个类被同一个类加载器加载才会是同一个类
- 双亲委派模型确保了基础类由顶层类加载器加载,避免了不同类加载器加载同一个类导致的类型混乱
- 例如:如果没有双亲委派模型,不同的类加载器可能会加载多个不同的String类,导致类型转换异常
3. 实现类的隔离
- 不同层次的类加载器加载不同范围的类,实现了类的隔离
- 例如:应用程序类加载器无法加载扩展类加载器和启动类加载器加载的类的内部实现细节
4. 便于类的管理和扩展
- 可以通过自定义类加载器来加载特定路径的类
- 可以实现热部署、模块化等高级特性
六、破坏双亲委派模型的典型场景
双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐的类加载器实现方式。在Java发展历史上,出现过三次大规模破坏双亲委派模型的情况。
1. 第一次破坏:JDK 1.2之前
- JDK 1.2之前还没有引入双亲委派模型
- 类加载器只有一个抽象类
ClassLoader - 用户需要重写
loadClass()方法来实现自定义类加载逻辑 - JDK 1.2之后引入了双亲委派模型,但为了兼容旧代码,仍然允许重写
loadClass()方法
2. 第二次破坏:SPI机制(服务提供者接口)
- 问题:双亲委派模型无法解决"基础类调用用户代码"的问题
- 典型场景:JDBC、JNDI、JAXP等SPI机制
- 具体例子:
- JDBC的
Driver接口由启动类加载器加载 - 但具体的驱动实现(如MySQL的
com.mysql.cj.jdbc.Driver)由应用程序类加载器加载 - 启动类加载器无法加载应用程序classpath下的驱动实现类
- JDBC的
- 解决方案:
- 使用线程上下文类加载器(
Thread.getContextClassLoader()) - 基础类通过线程上下文类加载器来加载SPI实现类
- 这实际上是逆向使用类加载器,破坏了双亲委派模型的层次结构
- 使用线程上下文类加载器(
3. 第三次破坏:热部署、模块化
- 需求:实现类的热替换、模块的动态加载和卸载
- 典型场景:
- Tomcat等Web容器
- OSGi模块化框架
- Spring Boot DevTools
- 实现原理:
- 每个Web应用都有自己的类加载器
- 当应用需要热部署时,直接卸载整个类加载器,然后重新创建一个新的类加载器来加载更新后的类
- 这种方式打破了双亲委派模型的"向上委派"原则,实现了类的隔离和动态管理
4. 其他常见的破坏场景
- 自定义类加载器重写
loadClass()方法:不遵循双亲委派逻辑,直接自己加载类 - 类加载器的隔离需求:例如在同一个JVM中运行多个应用,需要隔离不同应用的类
- 字节码加密与解密:自定义类加载器加载加密后的字节码文件
七、总结与核心考点
1. 核心知识梳理
- 类加载机制是JVM的核心组成部分,负责将Class文件转换为运行时的Java类型
- 类加载过程包括加载、验证、准备、解析、初始化5个核心阶段
- 类加载器分为启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器
- 双亲委派模型是Java推荐的类加载器实现方式,核心是"向上委派,向下加载"
- 双亲委派模型的主要好处是保证核心类库的安全性和类的唯一性
- SPI机制、热部署等场景需要破坏双亲委派模型
2. 高频面试考点
- 类加载的5个阶段分别做了什么?
- 准备阶段和初始化阶段的区别是什么?
- 什么是双亲委派模型?它的工作原理是什么?
- 双亲委派模型有什么好处?
- 有哪些场景需要破坏双亲委派模型?为什么?
- 线程上下文类加载器是什么?它解决了什么问题?
- Tomcat的类加载器结构是怎样的?为什么要这样设计?
JVM类加载机制:背诵版问答卡片 + 深度补充
一、核心考点问答卡片(可直接背诵)
基础概念类
-
问:什么是JVM类加载机制?
答:虚拟机将Class文件中的二进制数据加载到内存,经过验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。核心特点是动态加载、按需加载和类型安全。 -
问:类加载的完整生命周期包括哪些阶段?哪些阶段顺序是确定的?
答:7个阶段:加载→验证→准备→解析→初始化→使用→卸载。其中加载、验证、准备、初始化和卸载的顺序是确定的,解析阶段可以在初始化之后开始(支持动态绑定)。
类加载过程类
-
问:加载阶段的主要任务是什么?二进制字节流有哪些来源?
答:任务:①通过全限定名获取二进制字节流;②将静态存储结构转化为方法区运行时数据结构;③生成java.lang.Class对象作为访问入口。
来源:本地文件、网络、压缩包(JAR/WAR)、运行时动态生成(ASM/动态代理)、数据库、加密文件解密。 -
问:验证阶段的目的是什么?主要验证哪些内容?
答:目的:确保Class文件信息符合虚拟机要求,不会危害虚拟机安全。
内容:文件格式验证→元数据验证→字节码验证→符号引用验证。 -
问:准备阶段和初始化阶段的核心区别是什么?
答:- 准备阶段:为静态变量分配内存并设置数据类型默认初始值(如int=0,引用=null);final修饰的静态常量直接赋值为代码指定值。
- 初始化阶段:执行类构造器
<clinit>()方法,执行静态变量的显式赋值和静态代码块中的语句。
-
问:解析阶段的任务是什么?符号引用和直接引用的区别是什么?
答:任务:将常量池内的符号引用替换为直接引用。- 符号引用:用一组符号描述目标,与虚拟机内存布局无关。
- 直接引用:直接指向目标的指针、相对偏移量或句柄,与虚拟机内存布局直接相关。
-
问:类初始化的触发条件(主动使用)有哪些?
答:①使用new创建类实例;②访问类的静态变量/静态方法;③反射调用类;④初始化子类时父类先初始化;⑤虚拟机启动时的主类(含main方法);⑥JDK7+动态语言支持场景。 -
问:()方法有什么特点?
答:①由编译器自动收集静态变量赋值和静态代码块合并产生;②子类<clinit>()执行前父类<clinit>()已执行完毕;③虚拟机保证多线程环境下<clinit>()被正确加锁同步。
类加载器类
-
问:JDK8及之前有哪些内置类加载器?分别负责加载什么?
答:- 启动类加载器(Bootstrap):C++实现,加载
JAVA_HOME/jre/lib下的核心类库。 - 扩展类加载器(Extension):Java实现,加载
JAVA_HOME/jre/lib/ext下的扩展类库。 - 应用程序类加载器(Application):Java实现,加载应用程序classpath下的用户类。
- 启动类加载器(Bootstrap):C++实现,加载
-
问:JDK9及以上的类加载器结构有什么变化?
答:①扩展类加载器被平台类加载器(Platform ClassLoader)取代;②每个模块有自己的类加载器;③双亲委派模型仍然存在,但实现方式调整为模块化委派。 -
问:类加载器的核心方法有哪些?分别有什么作用?
答:loadClass(String name):加载指定类,实现双亲委派逻辑。findClass(String name):查找类,自定义类加载器通常重写此方法。defineClass(...):将字节数组转换为Class对象。resolveClass(Class<?> c):链接指定的类。
双亲委派模型类
-
问:什么是双亲委派模型?它的工作流程是什么?
答:定义:除顶层启动类加载器外,其余类加载器都应有自己的父类加载器("双亲"指父加载器,非继承关系)。
流程:收到加载请求→先委派给父类加载器→父类无法加载时才自己加载。所有请求最终都会传送到启动类加载器。 -
问:双亲委派模型有哪些核心好处?
答:①保证Java核心类库的安全性(防止恶意篡改核心类);②保证类的唯一性(同一个类被同一个类加载器加载才是同一个类);③实现类的隔离;④便于类的管理和扩展。 -
问:为什么双亲委派模型能防止恶意篡改核心类?
答:任何自定义的java.lang.String类加载请求都会先委派给启动类加载器,启动类加载器只会加载JDK自带的String类,用户自定义的同名类永远不会被加载。
破坏双亲委派类
-
问:什么是破坏双亲委派模型?有哪些典型的破坏场景?
答:破坏:不遵循"向上委派,向下加载"的原则,子加载器直接加载本应由父加载器加载的类,或逆向委派。
典型场景:①JDK1.2之前的自定义类加载器;②SPI机制(JDBC/JNDI/JAXP);③热部署/模块化(Tomcat/OSGi/Spring Boot DevTools);④自定义类加载器重写loadClass()方法。 -
问:SPI机制为什么会破坏双亲委派模型?它是如何解决的?
答:问题:基础类(如JDBC的Driver接口)由启动类加载器加载,但具体实现类(如MySQL驱动)由应用程序类加载器加载,启动类加载器无法加载应用classpath下的类。
解决:使用线程上下文类加载器,基础类通过它来加载SPI实现类,实现了逆向委派。 -
问:线程上下文类加载器是什么?它解决了什么问题?
答:线程上下文类加载器是Thread类中的一个属性,默认继承自父线程,可通过Thread.getContextClassLoader()获取和设置。
解决了"基础类调用用户代码"的问题,打破了双亲委派模型的单向委派限制。
二、深度补充:典型破坏双亲委派场景详解
1. Tomcat Web容器类加载器结构
Tomcat为了实现Web应用隔离和热部署,设计了复杂的类加载器层次结构,完全打破了标准的双亲委派模型。
(1) Tomcat类加载器层次结构
启动类加载器(Bootstrap)
↓
平台类加载器(Platform) / 扩展类加载器(Extension)
↓
应用程序类加载器(Application) ← 加载Tomcat自身的类
↓
公共类加载器(Common) ← 加载Tomcat和所有Web应用共享的类
↓
Catalina类加载器 ← 加载Tomcat服务器私有的类
↓
Shared类加载器 ← 加载所有Web应用共享的类
↓
WebApp类加载器(每个Web应用一个) ← 加载当前Web应用的类
↓
JSP类加载器(每个JSP文件一个) ← 加载JSP编译生成的Servlet类
(2) Tomcat破坏双亲委派的原因
- Web应用隔离:同一个Tomcat实例可以运行多个Web应用,不同应用可能依赖不同版本的第三方库(如Spring 4和Spring 5)。如果使用标准双亲委派模型,所有应用都会共享同一个类加载器,会导致版本冲突。
- 热部署支持:当Web应用的代码更新时,只需要卸载对应的WebApp类加载器,然后重新创建一个新的类加载器来加载更新后的代码,不需要重启整个Tomcat服务器。
(3) Tomcat类加载器的工作规则
Tomcat的WebApp类加载器默认不遵循双亲委派模型,而是采用"先自己加载,再委派给父类"的策略:
- 当WebApp类加载器收到类加载请求时,首先尝试自己加载这个类
- 只有当自己无法加载时,才会委派给父类加载器(Shared→Common→Application)
- 但对于Java核心类库(如java.lang.*),仍然会委派给启动类加载器加载,保证核心类的安全性
2. OSGi模块化框架类加载机制
OSGi是Java的动态模块化规范,它将应用程序拆分为多个独立的Bundle(模块),每个Bundle都有自己的类加载器。OSGi的类加载机制是对双亲委派模型最彻底的破坏。
(1) OSGi类加载的核心特点
- 每个Bundle都有自己的类加载器:Bundle之间通过"导出-导入"机制来共享类。
- 类加载器之间是平等的:没有严格的父子层次关系,Bundle可以直接委派给其他Bundle的类加载器。
- 动态性:Bundle可以在运行时动态安装、启动、停止、更新和卸载。
(2) OSGi类加载的工作流程
- 当一个Bundle需要加载一个类时,首先检查自己的类路径
- 如果找不到,检查该类是否被其他Bundle导出
- 如果有多个Bundle导出了同一个类,根据OSGi的规则选择合适的Bundle
- 委派给导出该类的Bundle的类加载器进行加载
- 如果仍然找不到,再委派给父类加载器(启动类加载器、平台类加载器等)
(3) OSGi类加载的优势与挑战
- 优势:实现了真正的模块化,支持动态更新和版本管理,解决了大型Java应用的类版本冲突问题。
- 挑战:类加载机制非常复杂,容易出现"类加载地狱"问题(如ClassNotFoundException、NoClassDefFoundError、ClassCastException等)。
3. Spring Boot DevTools热部署原理
Spring Boot DevTools是Spring Boot提供的开发工具,支持应用的快速热部署。它的实现原理也是基于自定义类加载器。
(1) DevTools的类加载器结构
DevTools使用两个类加载器:
- 基础类加载器(Base ClassLoader):加载不经常变化的类(如第三方库、Spring框架核心类等)
- 重启类加载器(Restart ClassLoader):加载应用程序自己编写的类(如业务代码、配置类等)
(2) 热部署的工作流程
- 当应用启动时,基础类加载器加载所有第三方依赖和框架类
- 重启类加载器加载应用程序的业务类
- 当检测到代码变化时,DevTools会销毁当前的重启类加载器
- 创建一个新的重启类加载器,重新加载所有应用程序的业务类
- 基础类加载器保持不变,因此不需要重新加载第三方依赖,大大提高了热部署的速度
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)