面试官:"线上服务频繁发生 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参数。
面试技巧:
- 展现系统化的排查思路
- 强调监控和数据驱动的重要性
- 结合具体工具和命令说明
- 给出具体的优化建议和参数调整
- 展示预防和治理的整体方案