三、内存问题排查
3.1 内存问题分类
3.2 JVM内存结构回顾
┌─────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆 │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ 新生代 │ │ 老年代 │ │ 元空间 │ │ │
│ │ │ Eden/S0/S1│ │ Old │ │ Metaspace│ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 虚拟机栈 │ │ 本地方法栈 │ │ 程序计数器 │ │
│ │ (线程私有) │ │ (线程私有) │ │ (线程私有) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 直接内存(DirectBuffer) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.3 内存泄漏典型案例
// 案例1:静态集合持有对象引用
public class StaticListLeak {
private static List<byte[]> list = new ArrayList<>();
public void addData() {
// 不断添加,永远不清理
list.add(new byte[1024 * 1024]); // 1MB
}
// 解决方法:使用WeakHashMap或配置缓存淘汰策略
}
// 案例2:ThreadLocal未remove
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public void process() {
threadLocal.set(new byte[10 * 1024 * 1024]); // 10MB
// 业务处理...
// 忘记remove!
}
// 线程池中的线程复用,导致内存泄漏
// 解决方法:finally块中调用threadLocal.remove()
}
// 案例3:监听器/回调未注销
public class ListenerLeak {
private List<EventListener> listeners = new ArrayList<>();
public void register(EventListener listener) {
listeners.add(listener); // 注册后忘记注销
}
// 解决方法:使用WeakReference或提供unregister方法
}
// 案例4:动态代理导致的类加载器泄漏
// 解决方法:使用非持久化的类加载器或控制代理生成数量
3.4 内存问题排查工具实战
# 1. 查看堆内存使用情况
jstat -gc <PID> 1000 10
# 输出字段说明:
# S0C/S1C: Survivor0/1容量
# S0U/S1U: Survivor0/1使用量
# EC/EU: Eden容量/使用量
# OC/OU: 老年代容量/使用量
# MC/MU: 元空间容量/使用量
# YGC/YGCT: Young GC次数/总时间
# FGC/FGCT: Full GC次数/总时间
# GCT: GC总时间
# 2. 生成堆转储文件(dump)
jmap -dump:live,format=b,file=heap.hprof <PID>
# 3. 实时分析堆对象
jmap -histo:live <PID> | head -20
# 4. 使用jcmd(JDK 8+推荐)
jcmd <PID> GC.heap_info
jcmd <PID> GC.class_histogram
jcmd <PID> VM.native_memory
# 5. 使用Arthas分析
# 查看JVM内存分布
memory
# 查看堆对象统计
heapdump --live /tmp/heap.hprof
# 查看最占内存的对象
ognl '@com.alibaba.arthas.deps.org.apache.commons.lang3.ArrayUtils@toString(new java.util.ArrayList())'
3.5 堆内存分析(MAT/Eclipse MAT)
// MAT 使用技巧
// 1. 打开heap.hprof文件
// 2. 查看Leak Suspects Report(泄漏嫌疑报告)
// 3. 使用Histogram查看所有类的实例数
// 4. 使用Dominator Tree找出占用内存最大的对象
// 5. 使用OQL(对象查询语言)执行自定义查询
// OQL示例:查询所有byte数组
SELECT * FROM "[B"
// 查询特定类的实例
SELECT toString(s) FROM com.example.entity.Order s WHERE s.orderId.startsWith("TEST")
// 查看某个对象的GC Root路径
// 右键对象 → Merge Shortest Paths to GC Roots → exclude weak/soft references
3.6 GC调优实战
GC选择指南:
内存 < 4GB: -XX:+UseSerialGC
内存 4-8GB, 低延迟要求: -XX:+UseG1GC
内存 > 8GB, 吞吐量优先: -XX:+UseParallelGC
低延迟极端要求(<10ms): 使用ZGC或ShenandoahGC
G1GC调优参数:
-XX:G1HeapRegionSize=16m # Region大小
-XX:MaxGCPauseMillis=200 # 目标暂停时间
-XX:G1NewSizePercent=5 # 新生代初始比例
-XX:G1HeapWastePercent=5 # 允许浪费比例
-XX:ConcGCThreads=4 # 并发GC线程数
-XX:ParallelGCThreads=8 # 并行GC线程数
-XX:InitiatingHeapOccupancyPercent=45 # MixedGC触发阈值
# 查看GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
# GC日志分析工具
# GCeasy (https://gceasy.io)
# GCViewer
四、I/O问题排查
4.1 磁盘I/O问题
# 1. 查看磁盘I/O整体情况
iostat -x 1
# 关键指标:
# %util: 磁盘利用率(超过80%说明繁忙)
# await: 平均I/O等待时间(超过10ms说明慢)
# r/s, w/s: 每秒读写次数
# rkB/s, wkB/s: 每秒读写KB数
# avgqu-sz: 平均队列长度
# 2. 查看哪些进程在读写磁盘
iotop -o
# 3. 查看具体文件I/O(需要root)
lsof | grep deleted # 查看被删除但仍被进程占用的文件
# 4. 慢SQL导致的磁盘I/O问题
# 查看MySQL慢查询日志
# 使用Percona Toolkit的pt-query-digest分析
4.2 文件描述符耗尽
# 查看进程打开的文件描述符数量
lsof -p <PID> | wc -l
# 查看进程限制
cat /proc/<PID>/limits
# 查看系统级别限制
ulimit -n
# 临时修改
ulimit -n 65535
# 永久修改 /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
# 常见原因:
# 1. 连接池未关闭
# 2. 文件流未关闭
# 3. 临时文件未清理
// 文件流未关闭典型案例
public class FileLeak {
public void readFile(String path) {
try {
InputStream is = new FileInputStream(path);
// 业务处理
// 忘记在finally中关闭
} catch (IOException e) {
// 异常处理
}
}
// 正确写法
public void readFileCorrect(String path) {
try (InputStream is = new FileInputStream(path)) {
// 业务处理
} catch (IOException e) {
// 异常处理
}
}
}