每天一道面试题之架构篇|线上频繁Full GC排查实战指南

0 阅读3分钟

面试官:"线上服务频繁发生Full GC,CPU使用率飙升,响应时间变长,你会如何系统性排查和解决这个问题?"

Full GC(完全垃圾回收)是Java应用性能的"红色警报",频繁发生会导致应用暂停、响应变慢,严重影响用户体验。掌握Full GC排查是高级工程师的必备技能。

一、Full GC核心知识体系

Full GC触发条件

  • 老年代空间不足
  • 方法区(元空间)不足
  • System.gc()调用
  • JDK垃圾回收策略触发

GC性能指标三要素

// GC关键监控指标
GC频率:    < 1次/小时(正常),> 1次/分钟(异常)
GC耗时:    Young GC < 50ms,Full GC < 1s
吞吐量:     > 95%(GC时间/总时间)

二、排查工具箱准备

必备监控工具

# 1. 实时监控工具
jstat -gcutil <pid> 1000  # 每秒钟监控GC状态
jmap -heap <pid>         # 堆内存分析
jstack <pid>             # 线程快照分析

# 2. 日志分析工具
- GCViewer
- GCEasy
- Arthas

# 3. 线上诊断工具
- Arthas实时诊断
- Prometheus + Grafana监控
- APM工具(Pinpoint, SkyWalking)

三、四级排查实战流程

第一级:快速状态确认

// 1. 快速查看GC状态
jstat -gcutil <pid> 1000 5

// 输出示例:
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  100.00  90.12  95.67  98.30  96.26   2154   32.543   35     12.345   44.888

// 关键指标解读:
- O: 老年代使用率 > 95%  可能触发Full GC
- FGC: Full GC次数在短时间内快速增长
- FGCT: Full GC总耗时,单次超过1s需要关注

第二级:内存快照分析

// 2. 生成堆转储文件
jmap -dump:live,format=b,file=heapdump.hprof <pid>

// 3. 直方图分析对象分布
jmap -histo:live <pid> | head -20

// 输出示例:
 num     #instances         #bytes  class name
----------------------------------------------
   1:       1256789       805425896  [B   2:        234567       123456789  java.util.HashMap$Node   3:        123456        98765432  java.lang.String

第三级:实时线程诊断

# 4. Arthas实时诊断
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 常用命令:
dashboard                 # 整体系统监控
thread -n 3              # 最忙的3个线程
jad com.example.Class    # 反编译类文件
watch *Service* method   # 方法执行监控

第四级:GC日志深度分析

// 5. 开启详细GC日志(JVM参数)
-Xloggc:./logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M

// 6. 分析GC日志模式
[Full GC (Allocation Failure) 
  [PSYoungGen: 0K->0K(256000K)] 
  [ParOldGen: 512000K->511000K(512000K)] 
  512000K->511000K(768000K)
, [Metaspace: 12345K->12345K(106496K)]
, 1.234567 secs]

四、常见问题模式及解决方案

模式一:内存泄漏

// 典型案例:静态集合持续增长
public class MemoryLeak {
    private static final Map<String, Object> CACHE = new HashMap<>();
    
    public void addToCache(String key, Object value) {
        CACHE.put(key, value);  // 永不释放!
    }
}

// 解决方案:使用WeakHashMap或设置过期时间
private static final Map<String, Object> CACHE = new ConcurrentHashMap<>();
// 或者使用Guava Cache with expiration

模式二:大对象分配

// 典型案例:大数组直接进入老年代
public byte[] processLargeData() {
    byte[] largeData = new byte[10 * 1024 * 1024]; // 10MB → 直接老年代
    return largeData;
}

// 解决方案:分块处理或调整JVM参数
-XX:PretenureSizeThreshold=3145728  // 3MB以上对象直接老年代

模式三:元空间溢出

// 典型案例:动态类生成或反射滥用
for (int i = 0; i < 100000; i++) {
    Class<?> dynamicClass = defineClass("DynamicClass" + i, bytecode);
}

// 解决方案:调整元空间大小并监控
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=512M
-XX:+TraceClassLoading

五、实战代码:内存泄漏检测器

/**
 * 内存泄漏检测工具类
 * 定期检测内存增长模式
 */
@Slf4j
public class MemoryLeakDetector {
    private final MemoryMXBean memoryMXBean;
    private final Map<String, MemorySnapshot> snapshots;
    
    public MemoryLeakDetector() {
        this.memoryMXBean = ManagementFactory.getMemoryMXBean();
        this.snapshots = new ConcurrentHashMap<>();
    }
    
    /**
     * 记录内存快照
     */
    public void takeSnapshot(String name) {
        MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
        MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
        
        MemorySnapshot snapshot = new MemorySnapshot(
            heapUsage.getUsed(),
            heapUsage.getMax(),
            nonHeapUsage.getUsed(),
            System.currentTimeMillis()
        );
        
        snapshots.put(name, snapshot);
        log.info("内存快照[{}]: heap={}MB, nonHeap={}MB", 
                name, 
                snapshot.getHeapUsed() / 1024 / 1024,
                snapshot.getNonHeapUsed() / 1024 / 1024);
    }
    
    /**
     * 检测内存泄漏
     */
    public boolean detectLeak(String snapshot1, String snapshot2, long threshold) {
        MemorySnapshot s1 = snapshots.get(snapshot1);
        MemorySnapshot s2 = snapshots.get(snapshot2);
        
        if (s1 == null || s2 == null) {
            return false;
        }
        
        long heapGrowth = s2.getHeapUsed() - s1.getHeapUsed();
        long timeDiff = s2.getTimestamp() - s1.getTimestamp();
        
        if (timeDiff > 0 && heapGrowth > threshold) {
            log.warn("检测到可能的内存泄漏: {} -> {}, 增长: {}MB, 时间: {}s",
                    snapshot1, snapshot2,
                    heapGrowth / 1024 / 1024,
                    timeDiff / 1000);
            return true;
        }
        
        return false;
    }
    
    /**
     * 内存快照类
     */
    @Data
    @AllArgsConstructor
    static class MemorySnapshot {
        private long heapUsed;
        private long heapMax;
        private long nonHeapUsed;
        private long timestamp;
    }
}

六、JVM参数优化模板

# 生产环境推荐配置(JDK8+)
#!/bin/bash

# 堆内存设置
-Xms4g -Xmx4g                    # 堆大小固定,避免动态调整
-XX:NewRatio=2                   # 年轻代:老年代 = 1:2
-XX:SurvivorRatio=8              # Eden:Survivor = 8:1:1

# GC算法选择(G1GC推荐)
-XX:+UseG1GC                     # 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200         # 目标暂停时间200ms
-XX:G1HeapRegionSize=4m          # Region大小

# GC日志配置
-Xloggc:${LOG_DIR}/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=10M

# 内存溢出处理
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof
-XX:OnOutOfMemoryError="kill -3 %p"  # 发生OOM时执行脚本

# 监控参数
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy

# 元空间设置
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers

七、系统性排查 Checklist

第一步:现象确认

  • GC频率是否异常(FGC > 1次/分钟)
  • GC耗时是否过长(Full GC > 1秒)
  • 系统吞吐量是否下降(< 90%)

第二步:数据收集

  • 获取GC日志(最近24小时)
  • 生成堆转储文件(heapdump)
  • 收集线程快照(jstack)
  • 记录JVM参数配置

第三步:模式分析

  • 分析GC日志的时间模式
  • 识别内存增长的趋势
  • 定位占用内存最大的对象类型
  • 检查代码中的可疑模式

第四步:验证修复

  • 调整JVM参数
  • 修复代码中的内存泄漏
  • 部署监控验证效果
  • 建立预防机制

八、面试深度问答

Q1:如何区分内存泄漏和内存溢出?  A:  内存泄漏是对象无法被回收但不再使用,内存溢出是内存确实不够用。通过分析堆转储中对象的GC Root引用链来区分。

Q2:Young GC频繁和Full GC频繁有什么区别?  A:  Young GC频繁通常是因为 survivor 区设置过小或对象过早晋升,Full GC频繁是因为老年代空间不足或内存泄漏。

Q3:如何使用Arthas快速定位问题?  A:  使用 dashboard 看整体状态,thread 看线程阻塞,jad 反编译可疑类,watch 监控方法调用。

Q4:G1GC和CMS有什么区别?  A:  G1GC适合大堆内存,可预测停顿时间;CMS并发收集减少停顿,但容易产生碎片。现在推荐使用G1GC。

Q5:如何预防Full GC问题?  A:  建立监控告警,定期进行压力测试,代码审查避免内存泄漏,合理设置JVM参数。

面试技巧

  1. 展现系统化的排查思路
  2. 强调监控和数据驱动的重要性
  3. 结合具体工具和命令说明
  4. 给出具体的优化建议和参数调整
  5. 展示预防和治理的整体方案