JVM性能调优实战
JVM性能调优实战
调优目标
- 降低GC停顿时间
- 提高应用吞吐量
- 减少内存占用
- 避免OOM和内存泄漏
监控工具
jps - 查看Java进程
# 列出所有Java进程
jps -l
# 输出示例
12345 com.example.Application
67890 org.apache.catalina.startup.Bootstrap
jstat - GC统计信息
# 每1000ms输出一次GC统计,共输出10次
jstat -gcutil 12345 1000 10
# 输出示例
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 32.14 95.67 92.34 25 0.456 3 1.234 1.690
jmap - 内存映射
# 堆内存概览
jmap -heap 12345
# 导出堆转储文件
jmap -dump:format=b,file=heap.dump 12345
# 触发GC
jmap -histo:live 12345 | head -20
jstack - 线程转储
# 导出线程栈
jstack 12345 > thread.dump
# 查看死锁
jstack -l 12345
VisualVM
# 启动VisualVM
jvisualvm
# 或使用JDK Mission Control
jmc
堆内存调优
设置合理的堆大小
# 初始堆和最大堆设置相同,避免动态扩展
java -Xms4g -Xmx4g -jar app.jar
# 查看默认堆大小
java -XX:+PrintFlagsFinal -version | grep HeapSize
新生代与老年代比例
# 默认比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1
# 指定新生代大小
-XX:NewSize=1g # 新生代初始大小
-XX:MaxNewSize=1g # 新生代最大大小
# Eden与Survivor比例
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
调优示例
public class HeapTuningDemo {
// 问题:频繁Full GC
// 解决:增大老年代空间
// -Xmx8g -XX:NewRatio=3
// 问题:Minor GC太频繁
// 解决:增大新生代空间
// -Xmx4g -XX:NewSize=2g
// 问题:大对象直接进老年代导致Full GC
// 解决:调整大对象阈值
// -XX:PretenureSizeThreshold=3145728 (3MB)
}
GC调优策略
选择合适的收集器
# 低延迟应用
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# 高吞吐量应用
java -XX:+UseParallelGC -XX:ParallelGCThreads=8 -jar app.jar
# 超低延迟(JDK11+)
java -XX:+UseZGC -jar app.jar
GC日志分析
# Java 11+
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
# Java 8
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar app.jar
G1调优参数
# G1调优
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:G1HeapRegionSize=16m # Region大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发GC的堆占用比
-XX:G1ReservePercent=10 # 保留空间
内存泄漏排查
常见内存泄漏场景
// 1. 静态集合持有对象引用
public class MemoryLeak {
private static List<byte[]> cache = new ArrayList<>();
public void add(byte[] data) {
cache.add(data); // 对象无法被GC
}
}
// 2. 未关闭的资源
public void leak() {
FileInputStream fis = new FileInputStream("file.txt");
// 忘记关闭,资源泄漏
}
// 3. ThreadLocal使用后未清除
public void threadLocalLeak() {
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024]);
// 线程池场景下ThreadLocal不会自动清理
}
排查步骤
// 1. 使用jmap导出堆转储
// jmap -dump:format=b,file=heap.dump 12345
// 2. 使用MAT分析
// 打开heap.dump,查看Dominator Tree
// 3. 查看占用内存最多的对象
// 4. 分析GC Roots引用链
// 5. 找到泄漏点并修复
线程调优
# 线程栈大小
-Xss256k # 默认1MB,可减小到256k
# 查看线程数
jstack 12345 | grep -c "nid="
// 线程池调优示例
ExecutorService executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000) // 任务队列
);
常见问题与解决方案
| 问题 | 症状 | 解决方案 |
|---|---|---|
| 频繁Minor GC | E区满,GC时间短 | 增大新生代空间 |
| 频繁Full GC | O区满,GC时间长 | 增大堆空间,优化代码 |
| OOM | OutOfMemoryError | 检查内存泄漏,增大堆空间 |
| GC停顿长 | 响应时间波动大 | 使用G1或ZGC |
| CPU飙高 | 线程死循环 | jstack分析线程状态 |
总结
JVM调优是一个持续的过程,需要结合监控数据和业务特点进行。优先解决内存泄漏和代码问题,再考虑JVM参数调优。