-
演练目标
模拟一个"内存泄漏导致频繁 Full GC"的场景,并完成从发现、定位到取证的全过程。
环境配置:4GB 堆内存,G1 垃圾回收器。
故障现象:CPU 飙高,应用响应卡顿,GC 日志疯狂刷屏,但进程未崩溃(假死)。
-
准备工作:制造故障
2.1 编写模拟代码
创建文件 FullGCPressure.java。该代码会持续填充堆内存至 80% 以上,并反复震荡,迫使 G1 收集器陷入"清理-失败-清理"的死循环。
java
java
复制
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class FullGCPressure {
// 静态容器 (GC Root),模拟内存泄漏
private static final List<byte[]> LEAK_CONTAINER = new ArrayList<>();
public static void main(String[] args) {
System.out.println("=== Full GC 施压程序启动 (Heap: 4GB) ===");
// 目标:占用 3.2GB (4GB * 80%)
long TARGET_SIZE = 3200L * 1024 * 1024;
long currentSize = 0;
try {
while (true) {
// 1. 申请 1MB 内存 (使用随机数填充,防止操作系统 Zero Page 优化)
byte[] data = new byte[1024 * 1024];
data[0] = (byte) ThreadLocalRandom.current().nextInt();
LEAK_CONTAINER.add(data);
currentSize += (1024 * 1024);
// 2. 达到阈值后,微量清理,诱发 GC 震荡
if (currentSize > TARGET_SIZE) {
// 移除 100MB 旧数据,给 GC 一点希望,防止直接 OOM
int freeCount = 100;
for (int i = 0; i < freeCount; i++) {
if (!LEAK_CONTAINER.isEmpty()) {
LEAK_CONTAINER.remove(0);
}
}
currentSize -= (freeCount * 1024L * 1024L);
// 稍微停顿,让 GC 线程抢占 CPU
TimeUnit.MILLISECONDS.sleep(50);
}
// 无 sleep 极速分配,压榨 CPU
}
} catch (OutOfMemoryError e) {
System.out.println("OOM 崩溃,模拟结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 编译与启动
使用符合生产环境的参数启动(注意:假设当前用户为 robin)。
java
# 1. 编译
javac FullGCPressure.java
# 2. 启动 (指定 G1GC, 4GB 堆, 打印详细日志)
/usr/local/java8/bin/java -server \
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps \
-Xloggc:gc.log \
FullGCPressure &
/usr/local/java8/bin/java: 谁来跑? (Java 虚拟机程序)
-server -Xms4g ... -Xloggc:gc.log: 怎么跑? (配置内存、GC策略、日志路径等 JVM 参数)
FullGCPressure: 跑哪个? (告诉 JVM 加载这个类,并运行它的 main 方法)
&: 在哪跑? (在后台运行,不占用当前终端窗口)
3.排查实战:侦探工作 (The Detective)
3.1 确认故障现象
命令:top
现象:找到 java 进程(记下 PID,例如 8060),观察 %CPU 是否接近或超过 100%。
3.2 实锤 Full GC (关键步骤)
命令:jstat -gcutil <间隔ms>
注意:必须使用启动进程的用户身份执行,否则报错 Could not attach。
java
# 假设 PID 为 8060,用户为 robin
sudo -u robin /usr/local/java8/bin/jstat -gcutil 8060 1000
图片就不贴上去了
观察指标:
O (Old): 长期维持在 99.xx%。
FGC (Full GC Count): 数字在不断增加 (561 -> 562 -> 563)。
FGCT (Full GC Time): 时间增长迅速,说明 CPU 都在做 GC。
4 取证环节:导出堆转储 (The Forensics)
这是最容易踩坑的环节(权限不足 + 磁盘空间不足)。
4.1 准备存储目录 (解决 "No space left on device")(如果内存够就不需要再去新建一个文件目录 )
由于根目录 / 通常空间较小,我们需要找一个大磁盘(如 /docker 或 /data)。
java
# 1. 查看磁盘空间
df -h
# 2. 创建目录 (假设 /docker 空间足够)
sudo mkdir -p /docker/dump_data
# 3. 修改目录权限
sudo chown 777 /docker/dump_data
4.2 执行 Dump (解决 "Permission denied")
使用 jmap 导出内存快照。
java
# 导出
sudo -u /usr/local/java8/bin/jmap -dump:format=b,file=/docker/dump_data/heap_dump.hprof 8060
注:如果进程卡死无响应,可加 -F 参数强制导出(速度慢)。
4.3 准备下载 :sftp 下载远程机器文件
java
# 修改文件权限,允许其他人读取
sudo chmod 644 /docker/dump_data/heap_dump.hprof
5. 根因分析 (The Analysis)
5.1 下载文件
通过 SFTP 将 /docker/dump_data/heap_dump.hprof 下载到本地。
5.2 使用 MAT 分析
打开 Eclipse Memory Analyzer (MAT)。
加载 heap_dump.hprof。
点击 "Leak Suspects Report"。
结论:
报告会直接指出:java.util.ArrayList 占用了 80% 以上的堆内存。
查看引用链:发现是 FullGCPressure 类的 LEAK_CONTAINER 静态变量持有的。
对象内容:全是 byte[] 数组。
- 恢复环境 (Cleanup):演练结束后,务必清理现场,释放资源;或者调用stopFullGc就可以。
java
# 1. 杀掉模拟进程
kill -9 8060
# 2. 删除巨大的 Dump 文件 (释放磁盘空间)
sudo rm -rf /docker/dump_data/heap_dump.hprof
# 3. 删除日志
rm gc.log