一、FullGC为何如此让人头疼

每次系统卡顿的时候,打开监控一看,十有八九都是FullGC在搞事情。这玩意儿就像个不请自来的客人,一来就把整个系统搞得鸡飞狗跳。但FullGC频繁触发真的只是表面现象,背后往往隐藏着更深层次的问题。

举个例子,我们有个电商系统,大促期间频繁出现FullGC。通过jstat观察发现,老年代使用率经常达到98%以上。这就像一个小房间挤了太多人,每次都要费好大劲才能腾出点空间。

// 示例:模拟内存泄漏导致FullGC
public class MemoryLeakDemo {
    static List<byte[]> leakList = new ArrayList<>(); // 静态集合导致对象无法被回收
    
    public static void main(String[] args) {
        while (true) {
            byte[] data = new byte[1024 * 1024]; // 每次分配1MB
            leakList.add(data); // 添加到集合中永不释放
            try {
                Thread.sleep(100); // 模拟业务处理间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
// 运行参数:-Xms100m -Xmx100m -XX:+PrintGCDetails
// 很快就会看到频繁的FullGC日志

二、揪出FullGC的幕后黑手

FullGC频繁通常有四大元凶:内存泄漏、对象过早晋升、大对象分配和元空间问题。让我们一个个来剖析。

内存泄漏就像家里堆积的杂物,日积月累最终把空间占满。最常见的就是静态集合持有对象引用,或者资源未关闭。对象过早晋升则是年轻代对象没经过充分GC就被提升到老年代,就像小孩子过早承担大人责任。

// 示例:展示对象过早晋升问题
public class PrematurePromotion {
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];  // 256KB
        
        // 模拟业务处理
        for (int i = 0; i < 10; i++) {
            allocation2 = new byte[4 * _1MB];  // 4MB
            allocation3 = new byte[4 * _1MB];  // 4MB
            allocation3 = null;  // 释放引用
            allocation3 = new byte[4 * _1MB];  // 再次分配
        }
    }
}
// 运行参数:-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails
// 观察年轻代对象如何快速晋升到老年代

三、调优实战:对症下药

找到了问题,接下来就是开药方。针对不同场景,我们有不同的应对策略。

对于内存泄漏,首要任务是找到泄漏点。MAT工具是这方面的专家,它能帮你分析堆转储文件,找出是谁在持有不该持有的对象。如果是对象过早晋升,可能需要调整新生代大小或Survivor区比例。

// 示例:合理设置JVM参数解决FullGC问题
public class OptimizedJVMConfig {
    public static void main(String[] args) {
        List<byte[]> tempList = new ArrayList<>();
        while (true) {
            // 模拟正常业务:短期存活对象
            for (int i = 0; i < 100; i++) {
                byte[] shortLived = new byte[1024 * 100]; // 100KB
            }
            
            // 模拟长期存活对象
            byte[] longLived = new byte[1024 * 1024]; // 1MB
            tempList.add(longLived);
            
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 定期清理
            if (tempList.size() > 10) {
                tempList.clear();
            }
        }
    }
}
/*
推荐JVM参数:
-Xms512m -Xmx512m 
-XX:NewRatio=2 
-XX:SurvivorRatio=8 
-XX:+UseConcMarkSweepGC 
-XX:+CMSParallelRemarkEnabled 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=75
*/

四、避坑指南与最佳实践

调优不是一劳永逸的事,需要持续监控和调整。以下是一些实战经验:

  1. 监控先行:没有监控就谈不上调优。JMX、Prometheus + Grafana都是好帮手
  2. 循序渐进:每次只调整一个参数,观察效果后再决定下一步
  3. 关注业务:调优参数必须结合业务特点,没有放之四海而皆准的配置
// 示例:使用JMX监控内存使用情况
public class JVMMonitoring {
    public static void main(String[] args) throws Exception {
        // 获取内存MXBean
        MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean();
        
        // 启动监控线程
        new Thread(() -> {
            while (true) {
                MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage();
                System.out.printf("Heap used: %.2fMB, committed: %.2fMB, max: %.2fMB\n",
                    heapUsage.getUsed() / 1024.0 / 1024,
                    heapUsage.getCommitted() / 1024.0 / 1024,
                    heapUsage.getMax() / 1024.0 / 1024);
                
                try {
                    Thread.sleep(5000); // 每5秒采集一次
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // 模拟业务负载
        loadData();
    }
    
    private static void loadData() {
        List<byte[]> dataCache = new ArrayList<>();
        Random random = new Random();
        while (true) {
            if (random.nextInt(100) > 70) {
                dataCache.add(new byte[1024 * 1024]); // 30%概率分配1MB
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

五、总结与展望

FullGC问题就像侦探破案,需要抽丝剥茧找到真正原因。记住几个关键点:监控是基础,分析是手段,调优是结果。未来随着ZGC、Shenandoah等新GC算法的成熟,FullGC问题可能会越来越少,但理解其原理永远有价值。

最后送大家一句话:没有最好的配置,只有最适合的配置。调优之路,道阻且长,但掌握正确方法后,定能事半功倍。