本文适合刚入职需要排查线上问题的Java开发,以及正在准备面试的同学。内容从实际工作场景出发,配合代码演示,让你快速掌握JVM调优的核心技能。
一、为什么需要JVM调优?
先看一个真实场景:
// 线上服务运行一段时间后频繁Full GC,响应时间从50ms飙升到2s
// 通过调优后,GC时间从每次500ms降到50ms
常见问题表现:
-
接口响应变慢
-
频繁GC(通过日志或监控发现)
-
OOM(OutOfMemoryError)
-
CPU使用率飙高
二、JVM内存结构(必背)
┌─────────────────────────────────────────────────────────┐
│ JVM内存 │
├──────────────┬──────────────┬───────────────────────────┤
│ 堆(Heap) │非堆(Non-Heap)│ 线程私有 │
├──────────────┼──────────────┼───────────────────────────┤
│ 新生代 │ 方法区/元空间│ 虚拟机栈 │
│ ├─ Eden │ (Metaspace) │ 本地方法栈 │
│ ├─ S0 │ │ 程序计数器 │
│ └─ S1 │ │ │
│ 老年代 │ │ │
└──────────────┴──────────────┴───────────────────────────┘
一句话记忆:
-
堆:存放对象实例(new出来的都在这)
-
方法区/元空间:存放类信息、常量、静态变量
-
栈:每个线程私有,存放局部变量、方法调用
三、GC算法详解
3.1 三种基础算法
| 标记-清除 | 标记可回收对象,然后清除 | 简单 | 内存碎片 |
| 标记-整理 | 标记后,存活对象向一端移动 | 无碎片 | 效率较低 |
| 复制算法 | 将内存分两块,只使用一块 | 效率高、无碎片 | 浪费一半内存 |
3.2 新生代为什么要用复制算法?
// 新生代特点:98%的对象朝生夕死
// 所以新生代分为:Eden(80%) + S0(10%) + S1(10%)
// 对象创建流程:
Object obj = new Object(); // 1. 分配在Eden区
// 2. Eden满时触发Minor GC
// 3. 存活对象复制到S0或S1
// 4. 对象年龄+1(默认15岁晋升老年代)
3.3 常见垃圾收集器
| Serial | 新生代 | 单线程,STW | 客户端程序 |
| ParNew | 新生代 | 多线程并行 | 配合CMS |
| Parallel Scavenge | 新生代 | 吞吐量优先 | 后台计算 |
| CMS | 老年代 | 低延迟 | Web应用 |
| G1 | 全堆 | 可预测停顿 | 大内存(6G+) |
| ZGC | 全堆 | 超低延迟(<10ms) | 大内存低延迟 |
四、常用JVM参数速查
4.1 内存设置
# 堆内存
-Xms4g # 初始堆大小(建议和Xmx一样,避免动态扩缩)
-Xmx4g # 最大堆大小
-Xmn2g # 新生代大小(通常为堆的1/3到1/2)
# 元空间(JDK8+)
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大值
# 栈大小
-Xss512k # 每个线程栈大小
4.2 GC设置
# 使用G1收集器(推荐JDK9+默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间
# 使用CMS收集器(JDK8常用)
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70 # 老年代70%触发CMS
# GC日志(排查问题必备)
-Xlog:gc*:file=gc.log:time,uptime,level,tags # JDK9+
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log # JDK8
4.3 OOM时自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
五、实战:线上问题排查全流程
5.1 模拟一个内存泄漏场景
import java.util.ArrayList;
import java.util.List;
/**
* 模拟内存泄漏:不断往List添加对象,却不释放
* 启动参数:-Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError
*/
public class MemoryLeakDemo {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
// 模拟不断接收请求,处理后缓存数据
for (int i = 0; i < 1000; i++) {
try {
// 每次分配1MB
byte[] data = new byte[1024 * 1024];
list.add(data); // 关键问题:只加不删!
System.out.println("第" + i + "次分配,当前list大小:" + list.size());
} catch (OutOfMemoryError e) {
System.out.println("OOM发生!已分配次数:" + i);
e.printStackTrace();
break;
}
}
}
}
运行结果:
第0次分配,当前list大小:1
第1次分配,当前list大小:2
…
第180次分配,当前list大小:181
OOM发生!已分配次数:181
java.lang.OutOfMemoryError: Java heap space
5.2 排查步骤
第一步:确认问题
# 查看JVM进程ID
jps -l
# 查看堆内存使用情况
jmap -heap <pid>
# 输出示例:
# Heap Usage:
# Eden Space:
# capacity = 67108864 (64.0MB)
# used = 67108864 (64.0MB)
# 100.0% used
第二步:生成dump文件分析
# 方式1:手动导出
jmap -dump:format=b,file=dump.hprof <pid>
# 方式2:配置自动导出(推荐)
# 启动时加参数:-XX:+HeapDumpOnOutOfMemoryError
第三步:使用MAT或VisualVM分析
# 使用Eclipse MAT分析dump文件
# 1. 打开MAT -> File -> Open Heap Dump
# 2. 查看 Leak Suspects Report(自动分析泄漏点)
# 3. 查看 Dominator Tree(找出占用内存最大的对象)
六、G1收集器深度解析(面试高频)
6.1 G1内存布局
┌─────────────────────────────────────────────────────────┐
│ G1堆内存布局 │
├─────────────────────────────────────────────────────────┤
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ R1│ │ R2│ │ R3│ │ R4│ │ R5│ │ R6│ │ R7│ │ R8│ … │
│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ │
│ Region大小:1MB ~ 32MB(根据堆大小自动计算) │
│ 每个Region可以是:Eden / Survivor / Old / Humongous │
└─────────────────────────────────────────────────────────┘
6.2 G1工作流程
1. 初始标记 (Initial Mark) – STW,标记GC Roots直接引用
↓
2. 并发标记 (Concurrent Mark) – 与用户线程并发执行
↓
3. 最终标记 (Final Mark) – STW,处理并发标记遗留的引用
↓
4. 筛选回收 (Cleanup) – STW,选择回收价值高的Region
6.3 G1关键参数
# Region大小(必须是2的幂,范围1-32MB)
-XX:G1HeapRegionSize=16m
# 目标停顿时间(默认200ms)
-XX:MaxGCPauseMillis=200
# 触发GC的堆使用率(默认45%)
-XX:InitiatingHeapOccupancyPercent=45
七、CMS vs G1 面试对比
| 内存布局 | 连续的新生代+老年代 | Region化 |
| 碎片处理 | 无,需要Full GC整理 | 标记-整理,无碎片 |
| 停顿预测 | 不支持 | 支持(-XX:MaxGCPauseMillis) |
| 浮动垃圾 | 有(Concurrent Mode Failure) | 无 |
| 适用场景 | 中小堆(<8G) | 大堆(>6G) |
| JDK版本 | JDK9废弃,JDK14移除 | JDK9+默认 |
面试回答模板:
CMS追求低停顿,采用标记-清除算法,会产生内存碎片;G1将堆划分为多个Region,支持可预测的停顿时间模型,适合大内存场景。JDK9之后推荐使用G1。
八、常用监控命令速查
# 1. 查看JVM进程
jps -lvm
# 2. 查看堆内存概况
jmap -heap <pid>
# 3. 查看对象统计(按大小排序前20)
jmap -histo <pid> | head -20
# 4. 查看GC情况(每秒输出一次)
jstat -gcutil <pid> 1000
# 5. 线程栈dump
jstack <pid> > thread.txt
# 6. 查看JVM所有参数
java -XX:+PrintFlagsFinal -version
# 7. Arthas(阿里开源诊断工具,推荐)
# 下载:curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动:java -jar arthas-boot.jar
# 常用命令:
# dashboard – 总览
# thread – 线程信息
# heapdump – 导出堆dump
# trace – 方法耗时追踪
九、调优实战案例
案例1:接口响应慢,发现频繁Young GC
现象: 接口P99从50ms涨到200ms,GC日志显示每3秒一次Young GC
分析:
# GC日志分析
jstat -gcutil <pid> 1000
# S0 S1 E O M YGC YGCT FGC FGCT
# 0.00 98.12 100.00 45.23 95.6 1256 8.523 2 0.312
# Eden区100%,说明新生代太小,对象分配太快
调优方案:
# 调整前:-Xms512m -Xmx512m -Xmn128m
# 调整后:-Xms512m -Xmx512m -Xmn256m # 增大新生代
# 结果:YGC频率从3秒一次降到15秒一次,P99恢复到60ms
案例2:Metaspace OOM
现象: 运行一段时间后报 java.lang.OutOfMemoryError: Metaspace
分析:
// 常见原因:
// 1. 动态生成大量类(CGLIB、反射)
// 2. 大量JSP页面
// 3. 类加载泄漏
// 使用Arthas查看类加载情况
// dashboard命令可以看到Metaspace使用情况
调优方案:
# 适当增大Metaspace
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
# 如果持续增长,需要排查代码是否有类加载泄漏
案例3:CPU飙高排查
排查流程:
# 1. 找到CPU最高的Java进程
top -c
# 2. 找到该进程中CPU最高的线程
top -Hp <pid>
# 3. 将线程ID转为16进制
printf "%x\\n" <tid>
# 4. 导出线程栈,搜索该16进制ID
jstack <pid> | grep <tid_hex> -A 30
# 常见原因:死循环、频繁GC、锁竞争
十、面试高频问题精选
Q1:什么时候需要JVM调优?
答:
堆内存设置不合理(频繁GC或浪费内存)
接口响应时间不达标
出现OOM
CPU使用率异常
注意:不是所有问题都需要调优,先排查代码问题。
Q2:如何判断对象是否可以被回收?
答: 两种算法
-
引用计数法:有引用+1,失效-1,为0回收(有循环引用问题,JVM不用)
-
可达性分析:从GC Roots出发,不可达的对象可回收(JVM使用)
Q3:GC Roots有哪些?
答:
虚拟机栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
Q4:对象什么时候进入老年代?
答:
年龄达到阈值(默认15,-XX:MaxTenuringThreshold设置)
大对象直接进入老年代(-XX:PretenureSizeThreshold设置)
动态年龄判断:Survivor空间中相同年龄对象总和大于Survivor空间一半,该年龄及以上对象直接进入老年代
Q5:什么是STW?如何减少?
答: STW(Stop The World):GC时暂停所有用户线程
减少方法:
使用低延迟收集器(G1、ZGC)
合理设置堆大小,避免频繁GC
减少对象创建(对象池、复用)
十一、调优原则总结
不要为了调优而调优:先确认是JVM问题,再动手
监控先行:Prometheus + Grafana、Arthas等工具
小步调整:一次只改一个参数,观察效果
记录基线:调优前记录GC次数、耗时、内存使用情况
考虑业务场景:低延迟 vs 高吞吐,选择不同策略
十二、推荐阅读
-
《深入理解Java虚拟机》- 周志明(JVM圣经,必读)
-
Oracle官方GC调优指南
-
G1收集器官方文档
作者简介:Java开发工程师,专注JVM性能优化与高并发架构设计。
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎评论区讨论。




