一、回顾JVM的完整流程
1.1、JVM的结构
JVM包括类加载子系统、运行时数据区、执行引擎、本地方法接口、本地方法库。
| 核心模块 | 核心职责 |
|---|---|
| 类加载子系统 | 加载、验证、准备、解析、初始化.class文件 |
| 运行时数据区 | 存储程序运行时的所有数据(线程私有 / 共享区域) |
| 执行引擎 | 解释 / 编译字节码为机器码,驱动程序运行 |
| 本地方法接口 | 作为 Java 与 C/C++ 本地方法的通信桥梁 |
| 本地方法库 | 存放 JNI 调用的本地方法实现(如操作系统底层 API) |
1.2运行时数据区
运行时数据区包括:方法区、堆、虚拟机栈、本地方法栈、程序计数器。
1.2.1方法区
核心功能
存储类信息、常量、静态变量、即时编译器编译后的代码等元数据。
版本变迁
JDK7 及以前:称为永久区 (PermGen),使用堆内存,易触发OutOfMemoryError: PermGen space。
JDK8 及以后:元空间(Metaspace) 替代永久区,使用本地内存(不受 JVM 堆大小限制),默认仅受物理内存限制。
核心子区域:运行时常量池
存放编译期生成的字面量 (如字符串常量)和符号引用。
支持运行时动态添加常量(如String.intern()方法)。
1.2.2堆
核心功能: JVM 中最大的内存区域 ,存放对象实例和数组,堆是线程共享的。
内存划分
| 分区 | 细节分区 | 特点 | 垃圾回收算法 |
|---|---|---|---|
| 新生代 | Eden 区 + S0 区 + S1 区 | 存放新生对象,对象存活率低 | 复制算法 |
| 老年代 | - | 存放存活时间长的对象,对象存活率高 | 标记清除/压缩算法 |
1.2.3虚拟机栈
核心功能
每个方法执行时创建一个栈帧,存储:局部变量表、操作数栈、帧数据区。
异常情况
栈深度超限会触发StackOverflowError;栈扩展失败会触发OutOfMemoryError。
1.2.4本地方法栈
与虚拟机栈功能类似,但为 Native 方法服务。
异常类型与虚拟机栈一致。
HotSpot 虚拟机直接将本地方法栈与虚拟机栈合并。
1.2.5程序计数器
核心功能
-
记录当前线程执行的字节码指令地址
-
线程切换时恢复执行位置
异常情况
1、唯一不会抛出 OOM的区域。
2、若执行 Native 方法,计数器值为undefined。
1.3类加载子系统
核心功能
负责将字节码文件 从文件系统 / 网络加载到 JVM 中,并生成对应的Class对象,同时保证类加载的安全性和唯一性。
阶段
| 阶段 | 核心操作 | 关键细节 |
|---|---|---|
| 加载 | 通过类的全限定名读取.class文件到内存,生成Class对象 |
类加载器的核心动作,Class对象存放在方法区 |
| 验证 | 校验字节码的合法性(文件格式、元数据、字节码指令等) | 防止恶意字节码破坏 JVM 安全(如修改访问权限) |
| 准备 | 为类的静态变量分配内存,并设置默认初始值 | 例:static int a = 10 → 此阶段a被赋值为0(初始化阶段才赋值为 10) |
| 解析 | 将常量池中的符号引用 替换为直接引用 | 符号引用(如类名、方法名)→ 直接引用(内存地址) |
| 初始化 | 执行静态代码块和静态变量赋值,触发条件:1. 类被主动使用(如new对象、调用静态方法)2. 反射调用类3. 子类初始化触发父类初始化 |
此阶段才执行static int a = 10的赋值逻辑 |
类加载器 分为 3 类,遵循双亲委派模型进行类加载:
01、启动类加载器
- 底层由 C/C++ 实现,属于 JVM 内核部分,无 Java 对象对应
- 负责加载
JAVA_HOME/jre/lib下的核心类(如java.lang.String) - 无父类加载器
02、扩展类加载器
- Java 实现(
sun.misc.Launcher$ExtClassLoader) - 加载
JAVA_HOME/jre/lib/ext下的扩展类 - 父类加载器为启动类加载器
03、应用程序类加载器
- Java 实现(
sun.misc.Launcher$AppClassLoader) - 加载用户类路径(
classpath)下的自定义类 - 父类加载器为扩展类加载器
双亲委派模型的核心规则
- 类加载器收到加载请求时,优先委托父类加载器加载
- 只有父类加载器无法加载时,才由子类加载器自行加载
- 核心优势:防止核心类被篡改 (如自定义
java.lang.String会被拒绝加载)、保证类加载的唯一性
1.4直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区间。通常,访问直接内存的速度会优于Java堆。因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
1.5执行引擎
负责将字节码指令转换为机器码并执行,核心组件包括解释器 、即时编译器(JIT) 和垃圾回收器。
二、类加载子系统
2.1定义
类加载子系统 是JVM负责加载、验证、准备、解析和初始化字节码文件的核心模块,其核心产出是内存中的java.lang.Class对象------这个对象是程序访问类元数据的唯一入口,被存储在方法区中。
注意:
1、类加载的对象是"类"而非"对象":它处理的是.class文件对应的类结构,生成Class对象;而对象是基于Class对象通过new关键字创建的,属于后续内存分配环节。
2、类加载是"按需触发"的:JVM不会在启动时一次性加载所有类,而是在程序需要使用某个类时才触发加载,这种懒加载机制能减少内存占用。
2.2生命周期
类加载子系统包括加载、验证、准备、解析、初始化。
| 阶段 | 核心操作 | 关键特点 |
|---|---|---|
| 加载 | 通过全限定名读取字节流,生成Class对象雏形 | 由类加载器执行,是类加载的起点 |
| 验证 | 多维度校验字节码合法性 | 保障JVM安全,可选(通过-Xverify:none关闭) |
| 准备 | 为静态变量分配内存,设置默认初始值 | 不执行代码,仅分配内存 |
| 解析 | 符号引用 → 直接引用 | 可在初始化后执行(支持动态绑定) |
| 初始化 | 执行静态代码块、静态变量显式赋值 | 按需触发,线程安全 |
2.2.1加载
通过类的"全限定名",从文件系统、网络、jar包或动态生成等来源,读取.class文件的字节流,然后将其转化为JVM内部的运行时数据结构。这个过程由类加载器具体执行。
2.2.2验证
.class文件本质是二进制流,可能被篡改或伪造。验证阶段会对字节码进行多维度校验:
1、文件格式验证:校验字节流是否符合.class文件规范。
2、元数据验证:校验类的元数据信息。
3、字节码验证:校验字节码指令的合法性。
4、符号引用验证:校验常量池中的符号引用是否能找到对应的目标。
2.2.3准备
准备阶段的核心是为类的静态变量分配内存,并设置默认初始值。
静态变量的显式赋值不会在此时执行,而是留到初始化阶段。
2.2.4解析
解析阶段则是将常量池中的"符号引用"替换为"直接引用"------符号引用是用字符串描述的目标,直接引用是目标在内存中的具体地址。这一步相当于将"间接地址"转化为"直接地址",为后续方法调用、字段访问提供高效支持。
2.2.5初始化
执行静态代码块和静态变量的显式赋值逻辑。只有当类被"主动使用"时,才会触发初始化。
常见触发场景:
1、main方法所在的类总会被首先初始化。
2、首次访问这个类的静态变量和静态方法。
3、子类初始化,如果父类还没初始化,会引发
4、子类访问父类的静态变量,只会触发父类的初始化
5、Class.forName
6、new关键字创建对象会导致初始化
常见未触发场景:
1、访问类的static final静态常量(基本数据类型和字符串)不会触发初始化。
2、类对象.class不会触发初始化
3、创建该类的数组不会触发初始化
4、类加载器的loadClass方法
5、Class.forName方法的第二个参数设置为false,不会触发初始化。

2.2.6使用
Class对象被用于创建实例、调用静态方法、访问静态字段等,这是程序运行的核心阶段;
2.2.7卸载
当类的Class对象不再被任何引用,且加载该类的类加载器也被回收时,JVM会将类的元数据从方法区中删除,完成类的卸载。
注意:JVM默认的类加载器(如启动类加载器)加载的核心类几乎不会被卸载,因为它们始终被JVM自身引用。
2.3出现原因
2.3.1统一"字节码"到"运行时结构"的转化
类加载子系统可以将字节码转化为当前平台可识别的运行时结构。
Java源码编译后生成的不是机器码,而是与平台无关的字节码。但不同操作系统的底层指令集不同。类加载子系统屏蔽了不同平台的差异,让字节码能在任何实现了JVM规范的环境中运行。
2.3.2通过内存管理与懒加载优化资源占用
类加载子系统的"按需加载"机制,能让类只在需要时才被加载到内存,大幅减少启动时的内存开销,提升程序启动速度。同时,它将Class对象统一存储在方法区,也便于JVM对类元数据进行集中管理。
2.3.3有效过滤恶意代码
Java是一门支持网络编程的语言,字节码可能来自网络,存在被篡改的风险。如果直接执行未经校验的字节码,可能会破坏JVM的内存结构、窃取数据。类加载子系统通过严格的校验规则,确保只有合法的字节码才能进入JVM运行,守护运行时安全。
2.3.4支持灵活的类加载场景
类加载子系统通过可扩展的类加载器架构在运行时动态读取字节码并加载,开发者可以通过自定义类加载器,实现从非标准来源加载类,提升了Java的灵活性。满足了这种动态扩展需求。
2.4双亲委派机制
2.4.1类加载器分类
在JVM中,类加载器不是单一的,而是分为三层,形成层级结构。
启动类加载器 :最顶层的类加载器,由C/C++实现(HotSpot虚拟机),负责加载JDK核心类库。它没有对应的Java对象,也无法被开发者直接访问。
扩展类加载器:由Java实现,负责加载扩展类库(如JAVA_HOME/jre/lib/ext下的jar包)。它的"父加载器"是启动类加载器。
应用程序类加载器:也称系统类加载器,由Java实现,负责加载用户类路径下的自定义类。它的"父加载器"是扩展类加载器,也是开发者默认使用的类加载器。
开发者还可以通过继承ClassLoader类实现自定义类加载器,用于加载非标准来源的类。
2.4.2双亲委派机制的核心规则
当一个类加载器收到类加载请求时,它不会先自己尝试加载,而是遵循"先委派父加载器加载,父加载器加载失败再自己加载"的规则。
1、类加载器收到加载请求后,首先将请求委派给它的"父加载器"。
2、父加载器收到请求后,继续委派给上一层父加载器,直到请求到达启动类加载器。
3、启动类加载器尝试加载该类:如果能加载,则直接返回Class对象;如果不能加载,则将请求回传给下一层加载器。
4、以此类推,直到某个类加载器成功加载该类并返回Class对象;如果所有父加载器都无法加载,最终由发起请求的类加载器自己尝试加载,若仍失败则抛出ClassNotFoundException异常。
2.4.3双亲委派机制的核心优势
1、防止核心类被篡改
假设开发者自定义了一个java.lang.String类,如果没有双亲委派机制,这个类可能会被应用程序类加载器加载,从而替换JDK的核心String类,导致安全风险。而有了双亲委派机制,自定义的java.lang.String会被委派给启动类加载器,启动类加载器发现自己已经加载过核心String类,就会直接返回已加载的Class对象,拒绝加载自定义的String类。
2、保证类的唯一性
同一个类只会被加载一次。因为无论哪个类加载器发起请求,最终都会委派到同一个父加载器加载,避免了同一个类被多个类加载器加载生成多个Class对象的情况。
2.4.4破坏双亲委派机制的场景
1、SPI机制:JDBC的核心接口在核心类库(由启动类加载器加载),但实现类在第三方jar包(由应用程序类加载器加载),启动类加载器无法加载第三方类,因此需要通过线程上下文类加载器(Thread Context ClassLoader)打破双亲委派,让核心类能加载第三方实现类。
2、自定义类加载器:开发者通过重写ClassLoader的loadClass()方法,可改变双亲委派的逻辑(如Tomcat的类加载器,为了实现Web应用的隔离,自定义了类加载顺序)。
三、 类加载子系统相关高频面试题
3.1、什么是JVM类加载子系统?它的核心产出是什么?
1、定义:类加载子系统是JVM负责加载、验证、准备、解析和初始化字节码(.class文件)的核心模块;
2、核心产出:内存中的java.lang.Class对象(存储在方法区),该对象是程序访问类元数据的唯一入口;
3、加载的是"类"而非"对象",采用"按需加载"机制,避免启动时内存过载。
3.2、类加载子系统的核心作用有哪些
1、加载字节码:通过全限定名读取.class文件字节流,打通"文件→内存"通道;
2、安全验证:多维度校验字节码合法性,过滤恶意代码;
3、资源准备:为静态变量分配内存并设置默认初始值,解析符号引用为直接引用;
4、初始化执行:执行静态代码块和静态变量显式赋值,保证类初始化的线程安全;
5、保障唯一性:配合双亲委派机制,确保类在JVM中唯一;
3.3类加载子系统是如何产生的?核心设计初衷是什么?
1、跨平台需求:统一字节码到不同平台运行时结构的转化,支撑Java"一次编写,到处运行";
2、资源优化:按需加载类,减少启动时内存占用,提升启动速度;
3、安全防护:通过验证阶段过滤恶意字节码,避免破坏JVM运行安全;
4、动态扩展:支持自定义类加载器,实现从非标准来源加载类,适配插件化、动态代理等场景。
3.4、类的生命周期包含哪些阶段?类加载子系统负责哪些阶段?
完整生命周期:7个阶段------加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载;
类加载子系统负责阶段:前5个核心阶段(加载、验证、准备、解析、初始化),执行顺序基本固定,仅解析阶段可在初始化后执行;
后续阶段:使用和卸载由JVM其他模块负责,核心类(如java.lang.String)几乎不会被卸载。
3.5、请详细说明类加载5个核心阶段的具体操作?
1、加载:通过全限定名读取.class字节流,转化为JVM内部运行时数据结构,生成Class对象雏形;
2、验证:安全校验,含文件格式验证、元数据验证、字节码验证、符号引用验证;
3、准备:为静态变量分配内存,设置默认初始值;
4、解析:将常量池中的符号引用替换为直接引用;
5、初始化:执行静态代码块和静态变量显式赋值,仅"主动使用"时触发;
3.6、什么是双亲委派机制?请描述其核心流程和优势?
1、定义:类加载器加载类时的优先级规则,"父子"为组合关系而非继承;
2、核心流程:
-
类加载器收到加载请求,先委派给父加载器;
-
父加载器继续向上委派,直至启动类加载器;
-
顶层加载器尝试加载,成功则返回Class对象;失败则回传请求,由下一层加载器尝试;
-
所有父加载器失败后,由发起请求的加载器自行加载,失败抛ClassNotFoundException;
3、核心优势:
-
安全:防止核心类被自定义类篡改;
-
唯一:保证同一类(全限定名相同)在JVM中仅被加载一次。
3.7JVM中的类加载器有哪些分类?各自的职责是什么?
① 启动类加载器:C/C++实现,加载JDK核心类库,无Java对象,无法直接访问;
② 扩展类加载器:Java实现,加载扩展类库,父加载器为启动类加载器;
③ 应用程序类加载器:Java实现,加载classpath下自定义类,父加载器为扩展类加载器,是开发者默认使用的类加载器;
④ 自定义类加载器:继承ClassLoader类,加载非标准来源类(加密文件、数据库),适配特殊业务场景。
3.8、类加载器的"父子关系"是继承关系吗?为什么?
不是继承关系,而是组合关系。
原因:子加载器通过"getParent()"获取父加载器引用,并非继承ClassLoader类实现"父子",核心是委派逻辑的层级传递 ,而非类的继承结构。
3.9、"堆中对象访问次数太多会被分配到直接内存,方便读取",该说法是否正确?为什么?
不正确
核心误区纠正:
1、直接内存是堆外本地内存,不属于标准运行时数据区,存储的是IO字节缓冲区数据,而非Java对象;
2、直接内存由开发者通过NIO的allocateDirect()主动申请,与对象访问次数无关;
3、类加载子系统仅负责加载类生成Class对象,不参与直接内存的分配,二者无关联。
3.10、类的初始化和加载有什么区别?哪些场景会触发类的初始化?
1、区别:加载是读取字节流生成Class对象雏形的过程;初始化是类加载的最后一步,核心是执行静态代码块和显式赋值;
2、触发初始化的场景:
01、main方法所在的类总会被首先初始化。
02、首次访问这个类的静态变量和静态方法。
03、子类初始化,如果父类还没初始化,会引发
04、子类访问父类的静态变量,只会触发父类的初始化
05、Class.forName
06、new关键字创建对象会导致初始化
3.11、ClassNotFoundException和NoClassDefFoundError的区别是什么?分别在什么场景下出现?
1、ClassNotFoundException:检查时异常,加载阶段触发。
场景:类路径错误、主动加载不存在的类。
2、NoClassDefFoundError:错误,链接初始化阶段触发。
场景:编译时存在类,运行时类丢失、静态代码块执行失败导致类初始化失败。
3.12、双亲委派机制是否可以被破坏?有哪些常见的破坏场景?
可以被破坏。
常见场景:
1、SPI机制:核心接口由启动类加载器加载,第三方实现类由应用程序类加载器加载,通过线程上下文类加载器打破委派。
2、自定义类加载器:重写ClassLoader的loadClass()方法,改变委派逻辑。
3、早期JDK版本(JDK1.2前):ClassLoader无双亲委派规范,可自定义破坏。
3.13、如何自定义类加载器?核心步骤是什么?
继承ClassLoader类:
1、重写findClass()方法:实现从自定义来源(如加密文件)读取.class字节流。
2、调用defineClass()方法:将字节流转化为Class对象。
3、可选重写loadClass():破坏双亲委派机制。
3.14、为什么JDK核心类无法被自定义类替换?
由双亲委派机制保障:
1、自定义的java.lang.String类被加载时,应用程序类加载器会先委派给父加载器(扩展→启动类加载器);
2、启动类加载器会优先加载自身负责的核心类库中的java.lang.String,不会加载自定义类;
3、若强制自定义,会触发安全校验失败,避免核心类被篡改。