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