目录
1.前言
作为Java生态的基石,Java虚拟机(JVM)承担着字节码解释执行、内存自动管理和跨平台兼容的核心职责。理解JVM工作机制,不仅是解决OutOfMemoryError
、StackOverflowError
等生产环境问题的关键,更是高级开发者必须掌握的系统级知识。
本文将系统剖析以下核心模块:
-
执行流程:从.class字节码到机器指令的转化流程
-
内存迷宫:堆/栈/方法区的数据存储结构与线程安全设计
-
类加载机制:双亲委派模型的安全保障与破坏场景
-
垃圾回收体系:从对象存活判定算法到收集器演进
带您深度剖析JVM,结合代码实例与内存快照,揭示JVM底层运作逻辑。
插播一条消息~
🔍十年经验淬炼 · 系统化AI学习平台推荐
系统化学习AI平台https://www.captainbed.cn/scy/
📚完整知识体系: 从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
💻 实战为王: 每小节配套可运行代码案例(提供完整源码)
🎯 **零基础友好:**用生活案例讲解算法,无需担心数学/编程基础
🚀 特别适合
- 想系统补强AI知识的开发者
- 转型人工智能领域的从业者
- 需要项目经验的学生
2.正文
2.1JVM运行流程
Java程序的执行依赖于Java虚拟机(JVM)。理解JVM的核心运行流程是掌握Java底层机制的基础。整个过程可以清晰地划分为以下几个关键阶段:
1.源码编译为字节码:
.java
源代码文件被javac
编译器处理。- 生成平台无关的 字节码 (
.class
文件),这是JVM的"机器语言",实现"一次编写,到处运行"。
2.启动JVM:
- 用户执行命令(如
java MyApp
)。 - 操作系统创建 JVM进程 并初始化运行环境。
3.类加载:
- JVM的 类加载器 查找并加载程序所需的
.class
文件(主类及其依赖)。 - 加载过程包含关键步骤:
- 加载: 读取字节码。
- 验证: 确保字节码安全合规。
- 准备: 为类的静态变量分配内存并赋默认值。
- 解析: (可稍后)将符号引用转换为直接引用。
- 初始化: 执行静态代码块,为静态变量赋程序设定的初始值。
4.内存分配(运行时数据区):
- JVM划分内存区域管理运行数据:
- 方法区: 存储类元信息、常量池、静态变量(JDK 8+ 使用元空间)。
- 堆: 存储所有对象实例和数组 ,是垃圾回收(GC) 的主战场。
- 虚拟机栈 (线程私有): 由栈帧组成,存储方法调用的局部变量、操作数栈等信息。
- 程序计数器 (线程私有): 记录当前线程执行的字节码指令地址。
- 本地方法栈 (线程私有): 支持调用本地方法(如C/C++代码)。
5.执行字节码:
- 执行引擎 负责执行加载的字节码:
- 解释器: 逐条解释执行字节码(启动快,效率较低)。
- 即时编译器 (JIT): 将高频执行的代码(热点代码) 动态编译成本地机器码,大幅提升执行效率。
- 垃圾收集器 (GC): 自动回收堆中不再使用的对象("垃圾")所占用的内存。
- 本地方法接口 (JNI): 提供Java代码与本地方法(如C/C++库)互调的标准机制。
6.程序执行与结束:
- 执行引擎找到并调用
main
方法(程序入口)。 - 程序逻辑运行:创建对象、调用方法、执行计算等。
- 程序结束条件:
main
方法正常结束。- 调用
System.exit()
。- 未捕获的异常/错误导致线程终止(若无其他非守护线程,则JVM退出)。
- 操作系统强制终止JVM进程。
图解:

2.2JVM运行时数据区

2.2.1堆(线程共享)
核心作用 :存储所有对象实例 与数组 。
特性:
生命周期:随JVM启动创建,退出销毁。
内存管理 :由垃圾收集器(GC)自动回收(主战场)。
分代设计(优化GC效率):
新生代(Young Generation):
Eden区(对象诞生地)
Survivor区(From + To,存活对象过渡区)
老年代(Old Generation):长期存活对象
元数据区(Metaspace,JDK8+):替代永久代(PermGen),存储类元信息
异常 :
OutOfMemoryError
(当堆无法分配新对象时抛出)。
2.2.2栈(线程私有)
核心作用 :存储方法调用的栈帧(Stack Frame) ,描述Java方法执行过程。
栈帧结构:
局部变量表(Local Variable Table):
- 存储方法参数、局部变量(基本类型+引用)
- 以Slot (32位)为最小单位(
long
/double
占2 Slot)
操作数栈(Operand Stack):
- 存储计算过程的临时数据(如算术运算的操作数)
- 基于栈的指令集核心(如
iadd
指令从栈顶弹出两个int
相加后压回)
动态链接(Dynamic Linking):
- 指向运行时常量池的方法引用(支持多态)
方法返回地址(Return Address):
- 方法退出时恢复上层方法执行点
- 异常 :
StackOverflowError
(栈深度超过-Xss
限制)。
2.2.3本地方法栈(线程私有)
核心作用 :支持本地方法(Native Method) 执行(如C/C++代码)。
特性:
与虚拟机栈功能类似,但服务于
native
方法(通过JNI调用)HotSpot等实现中常与虚拟机栈合并
异常 :StackOverflowError
(本地方法调用链过深)。
2.2.4程序计数器(线程私有)
核心作用 :记录当前线程正在执行的字节码指令地址 。
关键特性:
唯一无
OutOfMemoryError
的区域(占用极小固定内存)。执行Java方法时:记录字节码指令地址。
执行Native方法时:值为undefined(因执行引擎切换至本地代码)。
线程切换依赖:恢复线程执行时,依赖PC寄存器定位执行点。
2.2.5方法区(线程共享)
核心作用 :存储类元数据 、运行时常量池 、静态变量 等。
演进与实现:
JDK版本 | 实现方式 | 关键变化 |
---|---|---|
JDK ≤7 | 永久代(PermGen) | 受限于JVM堆内存(-XX:MaxPermSize ) |
JDK ≥8 | 元空间(Metaspace) | 使用本地内存 (-XX:MaxMetaspaceSize ) |
存储内容:
类信息(类名、父类、接口、字段描述符、方法字节码)
运行时常量池(符号引用 → 直接引用解析结果)
静态变量(
static
成员)JIT编译后的代码缓存(HotSpot的CodeCache)
异常 :OutOfMemoryError
(元空间耗尽时)。
2.2.6内存布局中的异常问题(堆溢出和栈溢出)
1. 堆溢出(OutOfMemoryError: Java heap space)
触发条件:
对象数量超过堆容量(如大数组、内存泄漏)
示例代码:
javaList<byte[]> list = new ArrayList<>(); while (true) { list.add(new byte[1024 * 1024]); // 持续分配1MB数组 }
解决方案:
增大堆:
-Xmx4g
(设置最大堆为4GB)分析内存泄漏:MAT、JProfiler等工具定位GC Roots引用链
2. 栈溢出(StackOverflowError)
触发条件:
方法调用链过深(如无限递归)
线程栈空间不足(
-Xss
设置过小)
示例代码:
javavoid recursiveCall() { recursiveCall(); // 无限递归 }
解决方案:
增大栈容量:
-Xss2m
(设置线程栈为2MB)优化代码:避免深层递归或循环依赖
总结:
区域 | 线程共享性 | 存储内容 | 异常类型 |
---|---|---|---|
堆 | 共享 | 对象实例、数组 | OutOfMemoryError |
虚拟机栈 | 私有 | 栈帧(局部变量、操作数栈) | StackOverflowError |
本地方法栈 | 私有 | Native方法调用信息 | StackOverflowError |
程序计数器 | 私有 | 当前指令地址 | 无 |
方法区 | 共享 | 类元数据、常量池、静态变量 | OutOfMemoryError (JDK8+) |
2.3JVM类加载
类加载是JVM将字节码文件(.class)加载到内存,并转换为运行时数据结构的过程。该过程分为加载(Loading) 、链接(Linking) 和初始化(Initialization) 三个阶段,其中链接又包含三个子阶段:
2.3.1类加载过程
2.3.1.1加载
核心任务:查找并加载类的二进制数据
加载源:本地文件系统、网络、ZIP/JAR包、运行时计算生成(动态代理)等
关键操作:
通过类的全限定名获取其二进制字节流
将字节流代表的静态存储结构转化为方法区的运行时数据结构
在堆中生成
java.lang.Class
对象,作为方法区数据的访问入口类加载器角色 :
ClassLoader.defineClass()
实现字节流到Class对象的转换
2.3.1.2链接
(1) 验证(Verification)
目的:确保字节码安全且符合JVM规范
文件格式验证:检查魔数(0xCAFEBABE)、版本号、常量池合法性
元数据验证:语义分析(是否有父类、是否实现接口等)
字节码验证:数据流和控制流分析(类型转换、跳转指令合法性)
符号引用验证:检查引用的类/字段/方法是否存在且可访问
(2) 准备(Preparation)
核心任务:为类变量分配内存并设置初始值
分配内存:在方法区为静态变量分配内存
初始值设置:赋予类型默认值(如
int=0
,boolean=false
,引用=null
)特殊处理 :
static final
常量直接赋程序设定值(编译期优化)
(3) 解析(Resolution)
核心任务:将符号引用转换为直接引用
符号引用:用一组符号描述引用的目标(全限定名)
直接引用:指向目标的指针、偏移量或句柄
解析类型:
类/接口解析
字段解析
方法解析
接口方法解析
2.3.1.3初始化
核心任务 :执行类构造器<clinit>()
方法
<clinit>
方法生成:
- 编译器自动收集类中所有静态变量赋值 和静态代码块
- 按源码顺序合并生成
执行规则:
- JVM保证子类
<clinit>
执行前父类<clinit>
已完成- 多线程环境下会被正确加锁同步(线程安全)
触发时机(首次主动使用时):
- 创建类实例(
new
)- 访问静态变量/方法(非常量)
- 反射调用(
Class.forName()
)- 初始化子类(父类需先初始化)
- 包含
main()
方法的启动类
java
[加载] → 获取字节流 → 创建Class对象 → 方法区数据结构
↓
[链接] → [验证] → 文件格式 → 元数据 → 字节码 → 符号引用
↓
[准备] → 静态变量分配内存 → 赋默认值
↓
[解析] → 符号引用 → 直接引用
↓
[初始化] → 执行<clinit>() → 静态赋值/代码块
2.3.2双亲委派模型
2.3.2.1模型结构
JVM采用树状层级结构的类加载机制:
java
启动类加载器(Bootstrap)
↑
扩展类加载器(Extension)
↑
应用程序类加载器(Application)
↑
自定义类加载器(Custom ClassLoader)
2.3.2.2核心加载器
(1) 启动类加载器(Bootstrap ClassLoader)
实现:C++编写(HotSpot),JVM内部组件
职责 :加载核心库(
JAVA_HOME/lib
目录)特性:
唯一无父加载器的加载器
加载
java.*
等核心包(如java.lang.String
)
(2) 扩展类加载器(Extension ClassLoader)
实现 :
sun.misc.Launcher$ExtClassLoader
(Java)职责 :加载扩展库(
JAVA_HOME/lib/ext
目录)特性:
父加载器是Bootstrap(但Java中表示为
null
)加载
javax.*
等扩展包
(3) 应用程序类加载器(Application ClassLoader)
实现 :
sun.misc.Launcher$AppClassLoader
职责:加载ClassPath用户类
特性:
默认的上下文类加载器
ClassLoader.getSystemClassLoader()
返回此加载器
2.3.2.3工作流程
当类加载请求发生时:
子加载器首先委托父加载器尝试加载
父加载器递归向上委托直至Bootstrap
若父加载器能完成加载,返回结果
若所有父加载器无法加载,子加载器才尝试加载
代码实现 (ClassLoader.loadClass()
简化逻辑):
java
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 到达Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法完成加载
}
// 4. 父加载器未找到,尝试自身加载
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
2.3.2.4破坏双亲委派的场景
(1) SPI机制(Service Provider Interface)
问题:JDBC等接口由Bootstrap加载,但实现类需由应用加载器加载
解决方案:线程上下文类加载器(Thread Context ClassLoader)
java// 获取上下文类加载器 ClassLoader loader = Thread.currentThread().getContextClassLoader(); // 加载服务实现 ServiceLoader<Driver> services = ServiceLoader.load(Driver.class, loader);
(2) OSGi模块化
机制:每个模块(Bundle)使用独立类加载器
特点:
平级类加载器间直接委托
支持模块热部署
(3) 热部署实现
技术原理:创建新的类加载器加载修改后的类
实现方式 :重写
findClass()
方法,不委托父加载器
类加载器关系图
java
[用户代码] → [自定义类加载器] → 委派 → [应用程序类加载器]
↓
委派 → [扩展类加载器]
↓
委派 → [启动类加载器]
2.3.2.5总结
类加载三阶段:
- 加载:二进制数据 → Class对象
- 链接:验证(安全)→ 准备(内存分配)→ 解析(符号→直接引用)
- 初始化:执行
<clinit>()
(静态初始化)
双亲委派核心价值:
安全屏障:防止核心API被篡改
- 示例:自定义
java.lang.String
类会被Bootstrap优先加载,用户类无效避免重复加载:父加载器已加载的类,子加载器不会再次加载
职责分离:不同加载器负责不同层次的类加载
破坏委派的合理场景:
- SPI服务加载(JDBC/JNDI)
- 模块化热部署(OSGi/Tomcat)
- 动态代码生成(Groovy/Scala REPL)
2.4垃圾回收GC
2.4.1死亡对象的判断算法
2.4.1.1引用计数法
核心原理:为每个对象维护引用计数器,记录被引用次数
java
Object A = new Object(); // A计数=1
Object B = A; // A计数=2
A = null; // A计数=1
B = null; // A计数=0 → 可回收
优点:
实现简单
实时性高,对象不再被引用时立即回收
致命缺陷:
循环引用问题:对象相互引用导致计数永不为0
javaclass Node { Node next; } Node a = new Node(); // a.count=1 Node b = new Node(); // b.count=1 a.next = b; // b.count=2 b.next = a; // a.count=2 a = b = null; // a.count=1, b.count=1 → 内存泄漏!
结论 :JVM不采用此算法
2.4.1.2可达性分析法
核心原理:从GC Roots对象出发,遍历引用链,不可达对象判定为死亡

GC Roots对象包括:
虚拟机栈中引用的对象
方法区静态属性引用的对象
方法区常量引用的对象
本地方法栈JNI引用的对象
同步锁持有的对象
JVM内部引用(如系统类加载器)
两次标记过程:
第一次标记:筛选
finalize()
方法未被覆盖或已被调用过的对象第二次标记:将对象放入F-Queue,由Finalizer线程执行
finalize()
若
finalize()
中对象重新建立引用链→自救成功
回收流程:
java
[GC Roots] → 枚举根节点 → 可达性分析 → 第一次标记 →
执行finalize() → 第二次标记 → 回收内存
2.4.2垃圾回收算法
2.4.2.1标记-清除法
执行步骤:
标记:遍历GC Roots标记可达对象
清除:回收未标记对象内存
java标记前: [A][B][C][D] (A、C可达) 清除后: [A][ ][C][ ] (B、D被回收)
缺点:
内存碎片化(不连续空间)
分配大对象时可能触发Full GC
2.4.2.2复制法
执行步骤:
将内存分为Eden、Survivor0、Survivor1区
对象首先分配在Eden
Minor GC时存活对象复制到Survivor1
清空Eden和Survivor0
交换Survivor0和Survivor1角色
java
GC前:
Eden: [A][B][C]
S0: [D][E]
S1: [空]
GC后:
Eden: [空]
S0: [空]
S1: [A][D] // B、C、E被回收
优点 :无碎片、高效
缺点 :内存利用率仅50%
应用:新生代回收
2.4.2.3标记-整理法
执行步骤:
标记:同标记-清除
整理:存活对象向一端移动
清理边界外内存
java
GC前: [A][ ][B][C][ ] (A、B、C存活)
GC后: [A][B][C][ ][ ] (无碎片)
优点 :无内存碎片
缺点 :移动对象成本高
应用:老年代回收
2.4.2.4分代算法
核心思想:按对象生命周期划分内存区域

内存分配策略:
新生代(占堆1/3)
Eden区(80%):新对象诞生地
Survivor区(20%):存活对象过渡区(S0+S1各占10%)
老年代(占堆2/3)
长期存活对象(年龄>15)
大对象直接进入(-XX:PretenureSizeThreshold)
对象晋升流程:

GC类型对比:
GC类型 | 触发条件 | 速度 | 停顿时间 | 算法 |
---|---|---|---|---|
Minor GC | Eden区满 | 快 | 短 | 复制算法 |
Major GC | 老年代满 | 慢 | 长 | 标记-清除/整理 |
Full GC | 老年代/方法区空间不足 | 极慢 | 很长 | 混合算法 |
2.4.3垃圾收集器
这里只做简单的介绍,并不过多展开:
1. 新生代收集器
收集器 | 并行方式 | 算法 | 特点 | 适用场景 | 启动参数 |
---|---|---|---|---|---|
Serial | 单线程 | 复制算法 | STW时间长,简单高效 | 客户端模式、嵌入式系统 | -XX:+UseSerialGC |
ParNew | 多线程 | 复制算法 | Serial的多线程版 | 需与CMS配合的老年代回收 | -XX:+UseParNewGC |
Parallel Scavenge | 多线程 | 复制算法 | 吞吐量优先 | 后台计算型应用 | -XX:+UseParallelGC |
2. 老年代收集器
收集器 | 并行方式 | 算法 | 特点 | 缺点 | 启动参数 |
---|---|---|---|---|---|
Serial Old | 单线程 | 标记-整理 | Serial的老年代版 | STW时间长 | -XX:+UseSerialGC |
Parallel Old | 多线程 | 标记-整理 | Parallel Scavenge的老年代版 | 无并发能力 | -XX:+UseParallelOldGC |
CMS | 并发 | 标记-清除 | 低延迟优先 | 内存碎片、并发失败 | -XX:+UseConcMarkSweepGC |
3. 全堆收集器(JDK 9+主流)
收集器 | 并行方式 | 算法 | 突破性特点 | 适用场景 | 启动参数 |
---|---|---|---|---|---|
G1 | 并发+并行 | 分区复制+标记整理 | 可预测停顿模型 (200ms内) | 大内存、低延迟要求 | -XX:+UseG1GC |
ZGC | 全并发 | 染色指针+读屏障 | 亚毫秒级停顿 (<10ms) | 超低延迟场景 | -XX:+UseZGC |
Shenandoah | 全并发 | 转发指针+读屏障 | 与ZGC竞争的低延迟收集器 | 类似ZGC |
3.小结
最后让我们总结文章核心内容:
内存管理:
- 堆 承载对象生命周期,是GC主战场;栈维系方法调用链,深度决定递归极限
- 元空间(Metaspace) 取代永久代,通过本地内存管理类元数据,显著降低OOM风险
- 程序计数器作为线程执行锚点,唯一免疫内存溢出的区域
核心机制:
- 双亲委派模型:通过层级加载保障类安全,SPI等场景需破坏此机制
- 分代收集理论:基于对象年龄(新生代/老年代)匹配标记-复制/标记-整理算法
- GC Roots可达性:以活动线程栈、静态变量等为根,判定对象存亡的绝对准则
JVM将物理内存抽象为逻辑数据区,将对象回收转化为算法问题。理解其内存布局与执行机制,是诊断性能瓶颈、规避内存泄漏的基石。正如Java语言架构师Brian Goetz所言:"虚拟机是Java生态的隐形操作系统"。掌握JVM,方能真正驾驭Java。