一、JVM 内存模型:程序运行的地基
1.1 运行时数据区域
JVM 在运行时会将内存划分为不同的区域,每个区域有不同的用途:
堆(Heap)
- 作用:存储对象实例,是 JVM 中最大的一块内存区域
- 特点:被所有线程共享,垃圾收集器的主要工作区域
- 分代设计:新生代(Young Generation)+ 老年代(Old Generation)
- 配置参数:
-Xms(初始大小)、-Xmx(最大大小)
// 对象在堆中创建
User user = new User(); // user 引用在栈,User 对象在堆
方法区(Method Area)
- 作用:存储类信息、常量、静态变量、即时编译后的代码
- 特点:线程共享,逻辑上是堆的一部分
- HotSpot 实现:JDK 7 叫永久代(PermGen),JDK 8+ 改为元空间(Metaspace)
- 配置参数:
-XX:MetaspaceSize、-XX:MaxMetaspaceSize
public class Config {
// 静态变量存储在方法区
private static final String VERSION = "1.0.0";
}
程序计数器(Program Counter Register)
- 作用:记录当前线程执行的字节码指令地址
- 特点:线程私有,唯一不会 OOM 的区域
- 为什么需要:多线程环境下,线程切换后能恢复到正确的执行位置
虚拟机栈(VM Stack)
- 作用:存储方法调用的栈帧(局部变量表、操作数栈、动态链接、返回地址)
- 特点:线程私有,生命周期与线程相同
- 常见异常:
StackOverflowError(递归过深)、OutOfMemoryError(无法分配新栈)
public void methodA() {
int a = 1; // 局部变量在栈帧中
Object obj = null; // 引用在栈,对象在堆
methodB(); // 新栈帧入栈
}
public void methodB() {
// 方法执行完毕,栈帧出栈
}
本地方法栈(Native Method Stack)
- 作用:为 Native 方法服务
- 特点:与虚拟机栈类似,但服务于 native 方法
1.2 内存模型与线程安全
JMM(Java Memory Model)定义了线程和主内存之间的抽象关系:
线程 ←→ 工作内存 ←→ 主内存
三大特性:
-
原子性:操作不可分割
synchronized、Lock、原子类保证
-
可见性:一个线程的修改对其他线程可见
volatile、synchronized、final保证
-
有序性:指令重排序问题
volatile禁止重排、happens-before原则
// volatile 保证可见性,禁止指令重排
private volatile boolean running = true;
public void stop() {
running = false; // 对其他线程立即可见
}
二、垃圾回收:自动内存管理的艺术
2.1 对象存活判定
引用计数法
- 原理:对象被引用时计数 +1,引用失效时 -1,计数为 0 时回收
- 缺点:无法解决循环引用问题
- 代表:Python、早期的 JVM
可达性分析(Reachability Analysis)
-
原理:从 GC Roots 出发,搜索走过的路径(引用链),不在链上的对象即为不可达
-
GC Roots 包括:
- 虚拟机栈中的引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- 同步锁持有的对象
- JVM 内部引用(基本类型的 Class 对象、常驻异常对象等)
GC Roots → 对象A → 对象B
→ 对象C
对象D(无引用链)→ 可回收
2.2 垃圾回收算法
标记-清除(Mark-Sweep)
- 过程:标记存活对象 → 清除未标记对象
- 缺点:效率不高、产生内存碎片
- 适用:老年代
标记-复制(Mark-Copy)
- 过程:将内存分为两块,存活对象复制到另一块,清空当前块
- 优点:没有碎片、效率高
- 缺点:内存利用率低(可用 50%)
- 优化:Eden:Survivor = 8:1(Appel 式回收)
- 适用:新生代
标记-整理(Mark-Compact)
- 过程:标记存活对象 → 向一端移动 → 清理边界外内存
- 优点:无碎片、内存利用率高
- 缺点:移动成本高
- 适用:老年代
分代收集(Generational Collection)
- 核心思想:根据对象存活周期将内存分代,不同代使用不同算法
- 新生代:对象朝生夕死,用复制算法
- 老年代:对象存活时间长,用标记-清除或标记-整理
2.3 垃圾收集器详解
Serial 收集器
- 特点:单线程、简单高效、需要 STW(Stop The World)
- 适用:客户端模式、小内存应用
- 参数:
-XX:+UseSerialGC
[GC pause (young)] → 单线程执行
Parallel Scavenge(并行清除)
- 特点:多线程、关注吞吐量(运行用户代码时间 / 总时间)
- 适用:后台计算、批处理场景
- 参数:
-XX:+UseParallelGC、-XX:MaxGCPauseMillis、-XX:GCTimeRatio
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
CMS(Concurrent Mark Sweep)
- 特点:并发收集、低停顿、基于标记-清除
- 缺点:CPU 敏感、有内存碎片、浮动垃圾问题
- 参数:
-XX:+UseConcMarkSweepGC
四个阶段:
- 初始标记(STW)—— 标记 GC Roots 直接关联的对象
- 并发标记 —— 从 GC Roots 遍历整个对象图
- 重新标记(STW)—— 修正并发标记期间变动的对象
- 并发清除 —— 清除标记的对象
G1(Garbage First)
-
特点:面向服务端、Region 化内存布局、可预测停顿
-
创新点:
- 将堆划分为多个大小相等的 Region
- 优先回收垃圾最多的 Region(Garbage First)
- 整体看是标记-整理,局部(两个 Region 之间)是复制算法
-
参数:
-XX:+UseG1GC、-XX:MaxGCPauseMillis
堆内存布局:
[Region0][Region1][Region2][Region3]...[RegionN]
E S O H ...
(Eden)(Survivor)(Old)(Humongous)
G1 工作模式:
- Young GC:Eden 区满时触发
- Mixed GC:老年代占用达到阈值时触发
- Full GC:回收速度跟不上分配速度时触发(应避免)
ZGC(JDK 11+)
-
特点:低延迟(停顿 < 10ms)、支持大内存(TB 级)
-
核心技术:
- 着色指针(Colored Pointers)
- 读屏障(Load Barrier)
- 并发整理
-
参数:
-XX:+UseZGC、-XX:ZCollectionInterval
Shenandoah(JDK 12+)
- 特点:与 ZGC 类似,低延迟、并发整理
- 区别:使用转发指针(Forwarding Pointer)而非着色指针
- 参数:
-XX:+UseShenandoahGC
2.4 垃圾收集器选择指南
| 收集器 | 特点 | 适用场景 | JDK 版本 |
|---|---|---|---|
| Serial | 简单单线程 | 客户端、小内存 | 所有 |
| Parallel | 高吞吐量 | 批处理、后台计算 | JDK 8 默认 |
| CMS | 低延迟 | Web 服务、交互式应用 | JDK 9 废弃 |
| G1 | 平衡吞吐与延迟 | 服务端通用 | JDK 9+ 默认 |
| ZGC | 超低延迟 | 大内存、对延迟敏感 | JDK 15+ |
| Shenandoah | 超低延迟 | 大内存、低延迟 | JDK 12+ |
# JDK 8 默认
-XX:+UseParallelGC
# JDK 9+ 默认
-XX:+UseG1GC
# 低延迟场景
-XX:+UseZGC -Xmx16g
三、JVM 调优实战:从理论到落地
3.1 调优目标
- 吞吐量优先:批处理、后台计算
- 延迟优先:Web 服务、实时交易
- 内存占用:容器化部署、资源受限环境
黄金法则:调优是权衡,没有完美方案
3.2 常用调优参数
内存配置
# 堆内存
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相等)
# 新生代
-Xmn2g # 新生代大小
-XX:NewRatio=2 # 老年代/新生代 = 2
-XX:SurvivorRatio=8 # Eden/Survivor = 8
# 元空间
-XX:MetaspaceSize=256m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小
# 直接内存
-XX:MaxDirectMemorySize=1g # 最大直接内存
垃圾收集器配置
# G1 收集器
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间目标
-XX:G1HeapRegionSize=16m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 老年代占用触发阈值
# ZGC
-XX:+UseZGC # 启用 ZGC
-XX:ZCollectionInterval=5 # GC 间隔(秒)
-XX:ZAllocationSpikeTolerance=2 # 分配峰值容忍度
# Parallel
-XX:+UseParallelGC # 新生代使用 Parallel
-XX:+UseParallelOldGC # 老年代使用 Parallel Old
-XX:ParallelGCThreads=8 # GC 线程数
GC 日志配置
# JDK 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
# JDK 9+(统一日志)
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags
3.3 实战案例
案例 1:频繁 Full GC
现象:
- 服务响应缓慢
- 日志中频繁出现 Full GC
- CPU 使用率飙升
分析步骤:
- 查看 GC 日志,确认 Full GC 频率和原因
- 使用
jstat -gcutil <pid>监控内存使用 - 使用
jmap -histo <pid>查看对象分布 - Dump 堆内存分析:
jmap -dump:format=b,file=heap.hprof <pid>
可能原因与解决方案:
| 原因 | 解决方案 |
|---|---|
| 老年代空间不足 | 增大堆内存或调整新生代比例 |
| 永久代/元空间不足 | 增大元空间 |
| 大对象直接进老年代 | 调整大对象阈值 -XX:PretenureSizeThreshold |
| 内存泄漏 | 分析堆转储,定位泄漏对象 |
# 示例配置
-Xms8g -Xmx8g -Xmn4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
案例 2:内存泄漏排查
场景:服务运行一段时间后 OOM
排查工具:
- jvisualvm / JConsole:实时监控
- MAT(Memory Analyzer Tool) :分析堆转储
- jhat:命令行堆分析工具
常见泄漏场景:
// 1. 静态集合类
public class Cache {
private static final Map<String, Object> cache = new HashMap<>();
// 忘记清理,无限增长
}
// 2. 监听器未注销
public class EventManager {
private List<Listener> listeners = new ArrayList<>();
// 添加后未移除
}
// 3. ThreadLocal 未清理
public class UserContext {
private static ThreadLocal<User> userHolder = new ThreadLocal<>();
// 线程池场景下,线程复用导致泄漏
}
解决方案:
- 使用弱引用:
WeakHashMap、WeakReference - 及时清理:
ThreadLocal.remove()、监听器注销 - 设置容量上限:Guava Cache、Caffeine
案例 3:大内存服务调优
场景:32GB 内存的服务,使用 G1 或 ZGC
G1 配置:
-Xms28g -Xmx28g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=32m \
-XX:InitiatingHeapOccupancyPercent=40 \
-XX:G1ReservePercent=15
ZGC 配置(JDK 17+):
-Xms28g -Xmx28g \
-XX:+UseZGC \
-XX:ZCollectionInterval=2 \
-XX:ZAllocationSpikeTolerance=4 \
-XX:+UnlockDiagnosticVMOptions \
-XX:+ZProactive
3.4 调优工具箱
命令行工具
# 查看 Java 进程
jps -l
# 查看堆内存使用
jstat -gc <pid> 1000 # 每秒打印一次
# 查看线程堆栈
jstack <pid> > thread_dump.txt
# 堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 查看 JVM 参数
jinfo -flags <pid>
可视化工具
| 工具 | 功能 | 适用场景 |
|---|---|---|
| JConsole | 实时监控 | 快速查看 |
| VisualVM | 监控 + 分析 | 综合诊断 |
| JProfiler | 深度分析 | 商业级调优 |
| Arthas | 在线诊断 | 生产环境 |
| GCViewer | GC 日志分析 | 分析 GC 模式 |
Arthas 实战
# 安装
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 常用命令
dashboard # 实时面板
thread # 线程信息
thread -n 5 # CPU 占用最高的 5 个线程
heapdump # 堆转储
jad com.xxx.Class # 反编译类
watch com.xxx.method returnObj # 观察方法返回值
四、总结与最佳实践
4.1 调优原则
- 不要过度调优:JVM 默认配置已经很好
- 先监控后调优:用数据说话
- 单变量实验:一次只改一个参数
- 生产验证:灰度发布、AB 测试
4.2 常见配置模板
微服务通用配置
-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heap.hprof \
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
高并发 Web 服务
-Xms8g -Xmx8g -Xmn4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:G1ReservePercent=15
批处理任务
-Xms16g -Xmx16g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=8 \
-XX:GCTimeRatio=19 # 目标吞吐量 95%
4.3 面试高频问题
-
JVM 内存区域有哪些?哪些会 OOM?
- 堆、方法区、虚拟机栈、本地方法栈、程序计数器
- 除程序计数器外都可能 OOM
-
Minor GC 和 Full GC 的区别?
- Minor GC:新生代 GC,频率高、速度快
- Full GC:整个堆 + 方法区,频率低、速度慢、需要优化
-
如何排查 OOM?
- 查看错误日志
- 分析堆转储(MAT)
- 定位大对象或内存泄漏
-
G1 和 CMS 的区别?
- G1:Region 化、可预测停顿、无碎片
- CMS:并发收集、有碎片、已废弃
-
什么时候该调优?
- 频繁 Full GC
- 响应时间不稳定
- 内存使用异常
写在最后
JVM 调优是一门实践艺术,理论只是基础。真正的高手是在无数次排查问题、分析日志、优化配置中成长起来的。
记住:
- 调优前先监控
- 调优要有目标
- 调优要有验证
希望这篇文章能帮你建立 JVM 知识体系,在实际工作中游刃有余!