一、Full GC为什么让人头疼

每次系统卡顿的时候,十有八九是Full GC在搞鬼。想象一下,你正在线上系统处理重要业务,突然监控告警疯狂提示"Full GC频率过高",这时候你的心情大概和看到早高峰地铁挤满人一样绝望。Full GC就像个霸道总裁,一旦它开始工作,整个JVM都得停下来等它,这就是所谓的"Stop-The-World"。

举个例子,我们有个电商系统,大促时频繁出现卡顿。用jstat一看,好家伙,Full GC每5分钟就来一次,每次停顿2秒以上。这就像收银台每隔五分钟就要关门盘点,顾客不投诉才怪。

二、揪出Full GC的罪魁祸首

要解决问题,先得知道问题出在哪。常见的Full GC诱因主要有三个:

  1. 老年代空间不足
  2. 永久代/元空间撑爆了
  3. System.gc()被乱调用

先看个内存泄漏的典型案例(示例基于Java 8):

// 错误示例:静态Map不断增长导致内存泄漏
public class ShoppingCartManager {
    private static Map<Long, List<Product>> userCarts = new HashMap<>();
    
    public void addToCart(long userId, Product product) {
        // 用户购物车数据永远不清理,最终撑爆老年代
        userCarts.computeIfAbsent(userId, k -> new ArrayList<>()).add(product);
    }
    
    // 正确做法应该增加清理机制
    public void clearCart(long userId) {
        userCarts.remove(userId);
    }
}

用jmap分析堆dump,你会发现老年代堆满了ShoppingCart对象。这就是典型的内存泄漏,对象该释放的时候没释放。

三、调优实战:对症下药

3.1 调整堆大小

堆太小会导致频繁GC,太大又会导致单次GC停顿时间长。建议通过以下步骤确定合理值:

  1. 用GC日志分析当前使用情况:
    java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar yourApp.jar
    
  2. 观察老年代使用峰值
  3. 设置初始(-Xms)和最大(-Xmx)堆大小为峰值的1.5倍

3.2 选择合适的GC算法

不同场景适合不同的GC算法。比如对于低延迟要求的系统,可以用G1:

# 使用G1 GC的推荐配置
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:InitiatingHeapOccupancyPercent=45 \
     -jar yourApp.jar

这个配置告诉JVM:

  • 使用G1收集器
  • 目标停顿时间200ms
  • 堆使用率达到45%时启动并发标记

3.3 元空间调优

如果问题是Metaspace撑爆了,可以这样调整:

# 限制元空间大小并开启压缩指针
java -XX:MaxMetaspaceSize=256m \
     -XX:+UseCompressedClassPointers \
     -jar yourApp.jar

四、避坑指南

  1. 不要随便调用System.gc()
    很多第三方库会偷偷调用它,可以通过参数禁用:

    -XX:+DisableExplicitGC
    
  2. 注意大对象分配
    比如一次性加载大文件到内存:

    // 错误示例:一次性读取大文件
    byte[] fileData = Files.readAllBytes(Paths.get("huge_file.bin"));
    
    // 正确做法:使用流式处理
    try (InputStream is = Files.newInputStream(Paths.get("huge_file.bin"))) {
        // 分块处理数据
    }
    
  3. 合理设置对象年龄阈值
    默认15次Young GC后对象会晋升到老年代,对于短命对象可以降低这个值:

    -XX:MaxTenuringThreshold=5
    

五、监控与持续优化

调优不是一劳永逸的事,需要持续监控。推荐配置:

  1. 开启GC日志详细记录:

    -XX:+PrintGCApplicationStoppedTime
    -XX:+PrintPromotionFailure
    -XX:+PrintTenuringDistribution
    
  2. 使用JMX监控关键指标:

    // 获取GC次数和耗时
    GarbageCollectorMXBean gcBean = ManagementFactory.getGarbageCollectorMXBeans().get(0);
    System.out.println("GC次数:" + gcBean.getCollectionCount());
    System.out.println("GC耗时:" + gcBean.getCollectionTime() + "ms");
    

六、总结

Full GC问题就像慢性病,需要定期体检(监控)、对症下药(调优)、养成良好的生活习惯(编码规范)。记住几个关键点:

  1. 先测量再优化,没有数据支撑的调优都是耍流氓
  2. 根据应用特性选择合适的GC算法
  3. 合理设置堆大小,不是越大越好
  4. 避免内存泄漏和大对象分配
  5. 持续监控,动态调整

调优是个系统工程,需要结合具体业务场景反复试验。希望这些经验能帮你少走弯路,让你的JVM跑得比兔子还快!