线上 Full GC 故障模拟

  1. 演练目标

    模拟一个"内存泄漏导致频繁 Full GC"的场景,并完成从发现、定位到取证的全过程。

    环境配置:4GB 堆内存,G1 垃圾回收器。

    故障现象:CPU 飙高,应用响应卡顿,GC 日志疯狂刷屏,但进程未崩溃(假死)。

  2. 准备工作:制造故障

    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[] 数组。

  1. 恢复环境 (Cleanup):演练结束后,务必清理现场,释放资源;或者调用stopFullGc就可以。
java 复制代码
# 1. 杀掉模拟进程
kill -9 8060
# 2. 删除巨大的 Dump 文件 (释放磁盘空间)
sudo rm -rf /docker/dump_data/heap_dump.hprof
# 3. 删除日志
rm gc.log
相关推荐
Coder_Boy_3 小时前
【Java核心】JVM核心知识清单
java·开发语言·jvm
hello 早上好4 小时前
07_JVM 双亲委派机制
开发语言·jvm
edisao5 小时前
第三章 合规的自愿
jvm·数据仓库·python·神经网络·决策树·编辑器·动态规划
wangluoqi8 小时前
c++ 数据结构-单调栈、单调队列 小总结
jvm·数据结构
今天你TLE了吗8 小时前
JVM学习笔记:第二章——类加载子系统
java·开发语言·jvm·笔记
CaracalTiger21 小时前
如何解决Unexpected token ‘<’, “<!doctype “… is not valid JSON 报错问题
java·开发语言·jvm·spring boot·python·spring cloud·json
江湖有缘1 天前
自托管RSS解决方案:Docker化Fusion安装教程
java·jvm·docker
Chan161 天前
《深入理解Java虚拟机》| 类加载与双亲委派机制
java·开发语言·jvm·面试·java-ee·intellij-idea
闻哥2 天前
GET和POST请求的本质区别
java·网络·jvm·spring·http·面试·https