深入理解 JVM:Java 程序员必知的虚拟机原理
本文面向初学者,带你深入理解 JVM 的核心概念和工作原理。
一、什么是 JVM?
1.1 从一个问题开始
你有没有想过,为什么 Java 程序可以在 Windows、Linux、Mac 上运行,而不需要重新编译?
这背后的大功臣就是 JVM(Java Virtual Machine),也就是 Java 虚拟机。
1.2 JVM 是什么?
简单来说,JVM 就是一个"翻译官"。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Java 代码 │ ──→ │ JVM │ ──→ │ 电脑硬件 │ │ (.java) │ │ (翻译官) │ │ (执行) │ └─────────────┘ └─────────────┘ └─────────────┘
工作流程:
-
你写的 Java 代码(.java 文件)
-
编译器把它变成字节码(.class 文件)
-
JVM 把字节码翻译成电脑能懂的指令
-
电脑执行这些指令
1.3 JVM 的组成部分
JVM 主要由四部分组成:
| 组件 | 作用 | 类比 |
|---|---|---|
| 类加载器 | 加载 .class 文件 | 快递员(送货上门) |
| 运行时数据区 | 管理内存 | 仓库(存放货物) |
| 执行引擎 | 执行字节码 | 工人(干活) |
| 本地接口 | 调用本地方法 | 翻译(对外沟通) |
┌─────────────────────────────────────────────────────────────┐ │ JVM 组成结构 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────────────────────────────┐ │ │ │ 类加载器 │ │ 运行时数据区 │ │ │ │ (ClassLoader)│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ └─────────────┘ │ │堆 │ │栈 │ │方法区│ │程序计│ │ │ │ │ │ │ │ │ │ │ │ │数器 │ │ │ │ ▼ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ ┌─────────────┐ └─────────────────────────────────────┘ │ │ │ 执行引擎 │ │ │ │ │(Execution │ ▼ │ │ │ Engine) │ ┌─────────────────────────────────────┐ │ │ └─────────────┘ │ 本地接口 │ │ │ │ │ (Native Interface) │ │ │ ▼ └─────────────────────────────────────┘ │ │ ┌─────────────┐ │ │ │ 本地方法库 │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘
1.4 JVM vs JRE vs JDK
很多人分不清这三个概念,其实很简单:
┌─────────────────────────────────────┐ │ JDK │ │ ┌───────────────────────────────┐ │ │ │ JRE │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ JVM │ │ │ │ │ └─────────────────────────┘ │ │ │ │ + 核心类库 │ │ │ └───────────────────────────────┘ │ │ + 开发工具(javac, java, etc.) │ └─────────────────────────────────────┘
| 名称 | 全称 | 包含内容 | 用途 |
|---|---|---|---|
| JVM | Java Virtual Machine | 虚拟机 | 执行字节码 |
| JRE | Java Runtime Environment | JVM + 核心类库 | 运行 Java 程序 |
| JDK | Java Development Kit | JRE + 开发工具 | 开发 Java 程序 |
简单记忆:
-
想运行 Java 程序?装 JRE
-
想开发 Java 程序?装 JDK
-
JVM 是 JRE 的核心部分
二、JVM 能干什么?
2.1 核心功能
JVM 的核心功能可以概括为四点:
1. 加载代码
-
将 .class 文件加载到内存
-
通过类加载器(ClassLoader)实现
2. 验证代码
-
检查字节码是否合法
-
防止恶意代码破坏系统
3. 执行代码
-
将字节码翻译成机器码
-
通过执行引擎实现
4. 内存管理
-
自动分配内存
-
自动回收内存(垃圾回收)
2.2 为什么需要 JVM?
跨平台
Java 代码 ──编译──→ 字节码 ──运行──→ Windows JVM ──执行──→ Windows │ ├──运行──→ Linux JVM ──执行──→ Linux │ └──运行──→ Mac JVM ──执行──→ Mac
安全性
-
沙箱机制:限制代码访问权限
-
字节码验证:防止恶意代码
-
访问控制:保护系统资源
自动内存管理
-
程序员不需要手动释放内存
-
垃圾回收器自动回收不再使用的对象
-
避免内存泄漏和内存溢出
性能优化
-
JIT 即时编译:热点代码编译成机器码
-
自适应优化:根据运行情况优化代码
-
多线程支持:充分利用多核 CPU
2.3 实际应用场景
| 应用场景 | 说明 | 示例 |
|---|---|---|
| Web 应用 | 服务器端开发 | Spring Boot、Tomcat |
| Android | 移动应用开发 | Android App |
| 大数据 | 数据处理 | Hadoop、Spark |
| 云计算 | 微服务架构 | Spring Cloud |
| 游戏开发 | 游戏服务器 | Minecraft 服务器 |
三、JVM 内存结构
3.1 运行时数据区
JVM 内存分为五个部分:
┌─────────────────────────────────────────────────────────────┐ │ JVM 内存结构 │ ├─────────────────────────────────────────────────────────────┤ │ 线程私有 │ 线程共享 │ │ ┌─────────────┐ │ ┌─────────────────────┐ │ │ │ 程序计数器 │ │ │ 堆(Heap) │ │ │ └─────────────┘ │ └─────────────────────┘ │ │ ┌─────────────┐ │ ┌─────────────────────┐ │ │ │ 虚拟机栈 │ │ │ 方法区(元空间) │ │ │ └─────────────┘ │ └─────────────────────┘ │ │ ┌─────────────┐ │ │ │ │ 本地方法栈 │ │ │ │ └─────────────┘ │ │ └─────────────────────────────────────────────────────────────┘
3.2 各部分详解
程序计数器(Program Counter Register)
-
线程私有:每个线程都有自己的程序计数器
-
作用:记录当前线程执行的字节码行号
-
特点:唯一不会出现 OOM 的区域
// 程序计数器示例
public void test() {
int a = 1; // 程序计数器记录这行
int b = 2; // 然后记录这行
int c = a + b; // 最后记录这行
}
虚拟机栈(VM Stack)
-
线程私有:每个线程都有自己的虚拟机栈
-
作用:存储方法调用的信息
-
组成:由一个个栈帧组成
┌─────────────────────────────────────────────────────────────┐ │ 虚拟机栈 │ ├─────────────────────────────────────────────────────────────┤ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 栈帧(Stack Frame) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ 局部变量表 │ │ 操作数栈 │ │ 动态链接 │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ │ │ │ │ │ 方法出口 │ │ │ │ │ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
本地方法栈(Native Method Stack)
-
线程私有:为 Native 方法服务
-
作用:存储本地方法调用的状态
-
示例:Object.hashCode()、Thread.start0()
堆(Heap)
-
线程共享:所有线程共享同一个堆
-
作用:存储对象实例
-
特点:垃圾回收的主要区域
┌─────────────────────────────────────────────────────────────┐ │ 堆(Heap) │ ├─────────────────────────────────────────────────────────────┤ │ 新生代(Young Generation) │ 老年代(Old Gen) │ │ ┌─────┬─────┬─────┐ │ ┌─────────────────┐ │ │ │ Eden│ S0 │ S1 │ │ │ │ │ │ │ 8 │ 1 │ 1 │ │ │ │ │ │ └─────┴─────┴─────┘ │ └─────────────────┘ │ │ 80% 10% 10% │ │ └─────────────────────────────────────────────────────────────┘
新生代:
-
Eden:新对象分配的区域
-
S0(Survivor 0):幸存者区域
-
S1(Survivor 1):幸存者区域
-
比例:8:1:1
老年代:
-
存放长期存活的对象
-
默认经过 15 次 GC 后进入老年代
方法区(Method Area)
-
线程共享:存储类信息
-
作用:存储类元数据、常量、静态变量
-
JDK 8 之后:改为元空间(Metaspace)
┌─────────────────────────────────────────────────────────────┐ │ 方法区(元空间) │ ├─────────────────────────────────────────────────────────────┤ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 类信息 │ │ │ │ - 类名、父类、接口 │ │ │ │ - 字段、方法 │ │ │ │ - 访问修饰符 │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 常量池 │ │ │ │ - 字面量 │ │ │ │ - 符号引用 │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 静态变量 │ │ │ │ - static 修饰的变量 │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
3.3 堆内存细分
堆内存分为新生代和老年代:
对象创建 ──→ Eden 区 │ ├── 存活 ──→ S0 区 │ │ │ ├── 存活 ──→ S1 区 │ │ │ │ │ ├── 存活 ──→ 老年代 │ │ │ │ │ └── 死亡 ──→ 被回收 │ │ │ └── 死亡 ──→ 被回收 │ └── 死亡 ──→ 被回收
四、垃圾回收机制
4.1 什么是垃圾回收?
垃圾回收(Garbage Collection,GC) 是 JVM 自动管理内存的机制。
为什么需要垃圾回收?
-
手动管理内存容易出错
-
内存泄漏会导致程序崩溃
-
垃圾回收让程序员专注业务逻辑
4.2 如何判断对象是否可回收?
引用计数法
每个对象有一个引用计数器:
-
有引用时 +1
-
引用失效时 -1
-
计数器为 0 时可回收
Object A ──引用──→ Object B │ │ │ ▼ └──────────────→ Object C(计数器:0,可回收)
缺点: 无法解决循环引用
// 循环引用示例 Object a = new Object(); Object b = new Object(); a.ref = b; // a 引用 b b.ref = a; // b 引用 a // 即使不再使用,引用计数器也不为 0
可达性分析法
从 GC Roots 开始搜索:
-
能到达的对象是存活的
-
不能到达的对象是可回收的
GC Roots │ ▼ Object1 ──→ Object2 ──→ Object3 │ ▼ Object4(可回收)
GC Roots 包括:
-
虚拟机栈中引用的对象
-
方法区中静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中引用的对象
4.3 垃圾回收算法
标记-清除算法
步骤: 1. 标记可回收对象 2. 清除标记对象 内存变化: [对象A][垃圾][对象B][垃圾][对象C] ↓ [对象A][ ][对象B][ ][对象C]
缺点: 产生内存碎片
复制算法
步骤: 1. 将内存分为两块 2. 只使用其中一块 3. 清空时将存活对象复制到另一块 内存变化: [对象A][对象B][对象C][ ][ ] ← 使用中 [ ][ ][ ][ ][ ] ← 空闲 ↓ [ ][ ][ ][ ][ ] ← 清空 [对象A][对象B][对象C][ ][ ] ← 复制过来
优点: 没有内存碎片 缺点: 内存利用率只有 50%
标记-整理算法
步骤: 1. 标记可回收对象 2. 将存活对象向一端移动 3. 清理边界外内存 内存变化: [对象A][垃圾][对象B][垃圾][对象C] ↓ [对象A][对象B][对象C][ ][ ]
优点: 没有内存碎片 缺点: 移动对象开销大
分代收集算法
根据对象存活周期,将内存分为不同区域:
| 区域 | 算法 | 原因 |
|---|---|---|
| 新生代 | 复制算法 | 对象存活率低 |
| 老年代 | 标记-整理算法 | 对象存活率高 |
4.4 常见垃圾回收器
| 回收器 | 特点 | 适用场景 |
|---|---|---|
| Serial | 单线程,简单高效 | 客户端程序 |
| ParNew | 多线程版本的 Serial | 服务端程序 |
| Parallel Scavenge | 关注吞吐量 | 后台计算 |
| CMS | 并发收集,低停顿 | 互联网应用 |
| G1 | 分区收集,可预测停顿 | 大内存应用 |
| ZGC | 超低停顿 | 大内存应用 |
CMS 收集器工作流程:
1. 初始标记(STW) 2. 并发标记 3. 重新标记(STW) 4. 并发清除
G1 收集器特点:
┌─────────────────────────────────────────────────────────────┐ │ G1 收集器 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ E │ E │ S │ O │ O │ H │ E │ S │ │ │ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │ E: Eden S: Survivor O: Old H: Humongous │ └─────────────────────────────────────────────────────────────┘
五、JVM 调优入门
5.1 为什么要调优?
-
减少 GC 停顿时间:提高用户体验
-
提高系统吞吐量:处理更多请求
-
避免内存溢出:保证系统稳定
5.2 常见 JVM 参数
堆内存设置
# 初始堆大小 -Xms512m # 最大堆大小 -Xmx1024m # 新生代大小 -Xmn256m # 示例:设置初始堆 512M,最大堆 1G java -Xms512m -Xmx1024m -jar app.jar
元空间设置
# 元空间初始大小 -XX:MetaspaceSize=128m # 元空间最大大小 -XX:MaxMetaspaceSize=256m
GC 设置
# 使用 G1 收集器 -XX:+UseG1GC # 最大 GC 停顿时间 -XX:MaxGCPauseMillis=200 # GC 线程数 -XX:ParallelGCThreads=4
日志设置
# 打印 GC 详细信息 -XX:+PrintGCDetails # 打印 GC 时间戳 -XX:+PrintGCDateStamps # 输出 GC 日志到文件 -Xloggc:gc.log
5.3 常见问题排查
内存溢出(OOM)
# 查看堆内存使用情况 jmap -heap <pid> # 查看对象占用情况 jmap -histo <pid> # 导出堆转储文件 jmap -dump:format=b,file=heap.hprof <pid>
CPU 使用率高
# 查看线程 CPU 使用情况 top -Hp <pid> # 导出线程栈 jstack <pid> > thread.txt
5.4 调优案例
案例:Web 应用频繁 Full GC
问题现象:
-
应用响应变慢
-
日志显示频繁 Full GC
排查过程:
-
查看 GC 日志,发现 Full GC 频率高
-
使用
jmap -heap查看堆内存,发现老年代接近满 -
使用
jmap -dump导出堆转储 -
使用 MAT 工具分析,发现内存泄漏
解决方案:
-
增加堆内存大小:
-Xms2g -Xmx2g -
修复内存泄漏:某个集合不断添加对象,未清理
-
调整 GC 参数:使用 G1 收集器
优化效果:
-
Full GC 频率从每分钟 1 次降到每小时 1 次
-
应用响应时间从 2 秒降到 200 毫秒
六、常见问题与面试题
6.1 常见问题
Q1:Java 为什么能跨平台?
A:因为 JVM 将 Java 代码编译成字节码,然后由不同平台的 JVM 解释执行。字节码是平台无关的,JVM 是平台相关的。
Q2:堆和栈的区别?
| 对比项 | 堆 | 栈 |
|---|---|---|
| 线程共享 | 是 | 否 |
| 存储内容 | 对象实例 | 局部变量 |
| 生命周期 | GC 管理 | 方法结束自动释放 |
| 大小 | 较大 | 较小 |
| 速度 | 较慢 | 较快 |
Q3:什么是内存泄漏?
A:对象不再使用,但无法被 GC 回收。常见原因:
-
集合类持有对象引用
-
未关闭的资源(数据库连接、文件流)
-
静态集合类
Q4:什么是内存溢出?
A:内存不足,无法分配新对象。常见原因:
-
堆内存设置过小
-
内存泄漏导致
-
对象过大
6.2 面试高频题
1. JVM 内存结构有哪些?
答:JVM 内存分为五部分:
-
程序计数器(线程私有)
-
虚拟机栈(线程私有)
-
本地方法栈(线程私有)
-
堆(线程共享)
-
方法区(线程共享)
2. 垃圾回收算法有哪些?
答:主要有四种:
-
标记-清除算法
-
复制算法
-
标记-整理算法
-
分代收集算法
3. 如何判断对象是否可回收?
答:两种方法:
-
引用计数法(有循环引用问题)
-
可达性分析法(主流方法)
4. 常见的垃圾回收器有哪些?
答:
-
Serial、ParNew、Parallel Scavenge
-
CMS、G1、ZGC
5. 如何进行 JVM 调优?
答:
-
监控 GC 日志
-
分析堆内存使用
-
调整 JVM 参数
-
修复内存泄漏
七、总结
核心知识点回顾
-
JVM 是什么:Java 虚拟机,实现跨平台
-
JVM 组成:类加载器、运行时数据区、执行引擎、本地接口
-
内存结构:堆、栈、方法区、程序计数器、本地方法栈
-
垃圾回收:自动内存管理,四种算法
-
JVM 调优:监控、分析、调参、修复
学习建议
-
先理解概念:不要急于深入细节
-
多动手实践:使用工具分析 JVM
-
关注面试题:巩固知识
-
循序渐进:先掌握基础,再学习高级特性
推荐资源
-
📚 《深入理解 Java 虚拟机》- 周志明
-
📖 JVM 官方文档
-
🛠️ VisualVM 工具
恭喜! 你已经掌握了 JVM 的核心知识。接下来,尝试在你的项目中实践吧!
如果你觉得这篇文章有帮助,欢迎点赞、收藏、关注!
附录:常用 JVM 命令
# 查看 JVM 版本 java -version # 查看 JVM 参数 java -XX:+PrintFlagsFinal -version # 查看进程 ID jps # 查看堆内存 jmap -heap <pid> # 查看线程栈 jstack <pid> # 查看 GC 日志 jstat -gcutil <pid> 1000 # 导出堆转储 jmap -dump:format=b,file=heap.hprof <pid>
更多推荐

所有评论(0)