一、垃圾回收机制是什么?

想象你家的垃圾桶,每天都会产生各种垃圾。如果没人清理,很快家里就会堆满垃圾,连走路的地方都没有。JVM的垃圾回收机制就像是这个清洁工,负责清理Java程序运行过程中产生的"垃圾"——那些不再被使用的对象。

垃圾回收器(GC)主要做三件事:

  1. 找出哪些对象是垃圾
  2. 把这些垃圾清理掉
  3. 整理内存空间,让新对象有地方住

举个例子,就像你玩俄罗斯方块,消掉一行后,上面的方块会自动下落填满空间。GC的工作机制也类似,只不过它处理的是内存空间。

二、为什么要调优垃圾回收?

默认的GC设置就像用一把万能钥匙开所有的锁——能用,但不够高效。调优GC主要是为了解决以下问题:

  1. 程序经常卡顿(STW停顿)
  2. 内存占用过高
  3. 吞吐量不够理想
  4. 响应时间不稳定

比如一个电商系统在大促时,如果GC没调好,可能会出现:

  • 用户下单时页面卡住几秒钟
  • 支付接口响应变慢
  • 服务器内存爆满导致崩溃

来看个实际案例:

// 技术栈:Java 8
// 一个典型的内存泄漏示例
public class OrderService {
    private static List<Order> orderCache = new ArrayList<>();
    
    public void processOrder(Order order) {
        // 把订单加入缓存
        orderCache.add(order);
        // 处理订单逻辑...
    }
    
    // 问题:订单处理完后没有从缓存中移除
    // 随着时间推移,orderCache会越来越大,最终导致OOM
}

这个例子中,订单处理完后没有清理缓存,就像家里只往垃圾桶扔垃圾,却从不倒垃圾一样,迟早会出问题。

三、常见的垃圾回收器

JVM提供了几种不同的"清洁工",各有特点:

  1. Serial GC:单线程清洁工,适合小房子(客户端应用)
  2. Parallel GC:多线程清洁工,适合打扫大房子(吞吐量优先)
  3. CMS:追求最短停顿时间的清洁工
  4. G1:分区打扫的智能清洁工(JDK9+默认)
  5. ZGC:超级清洁工,几乎不停顿(JDK11+)

以G1回收器为例,它的工作方式就像:

// 技术栈:Java 11
// 展示G1回收器的典型配置
public class G1Example {
    public static void main(String[] args) {
        // 设置堆内存为4G
        // -Xms4g -Xmx4g 
        // 使用G1回收器
        // -XX:+UseG1GC
        // 设置最大GC停顿时间目标为200ms
        // -XX:MaxGCPauseMillis=200
        // 设置并行GC线程数为4
        // -XX:ParallelGCThreads=4
        
        // 模拟一个长期运行的服务
        while (true) {
            // 不断创建新对象
            Object obj = new Object();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

G1把堆内存分成多个小块(Region),优先收集垃圾最多的小块,就像清洁工先打扫最脏的房间一样。

四、如何调优垃圾回收?

调优GC就像调整汽车的发动机,需要考虑多个参数:

  1. 堆内存大小:-Xms和-Xmx
  2. 新生代比例:-XX:NewRatio
  3. 回收器选择:-XX:+Use[GC名称]GC
  4. 停顿时间目标:-XX:MaxGCPauseMillis
  5. 并行线程数:-XX:ParallelGCThreads

来看一个电商系统的调优示例:

// 技术栈:Java 11
// 电商系统GC调优配置示例
public class EcommerceGCConfig {
    public static void main(String[] args) {
        // 推荐配置:
        // 初始堆内存4G,最大堆内存4G(避免动态调整开销)
        // -Xms4g -Xmx4g
        // 使用G1回收器
        // -XX:+UseG1GC
        // 最大GC停顿时间目标100ms
        // -XX:MaxGCPauseMillis=100
        // 设置并行GC线程数为CPU核心数的1/4
        // -XX:ParallelGCThreads=4
        // 开启GC日志记录
        // -Xlog:gc*=info:file=gc.log:time,uptime,level,tags
        
        // 模拟处理订单
        processOrders();
    }
    
    private static void processOrders() {
        List<Order> orders = new ArrayList<>();
        while (true) {
            // 模拟订单创建
            orders.add(new Order());
            
            // 每处理1000个订单清理一次
            if (orders.size() > 1000) {
                orders.clear(); // 手动清理,避免内存增长过快
            }
            
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这个配置适合中等规模的电商系统,平衡了吞吐量和响应时间。

五、生产环境实战技巧

  1. 监控先行:没有数据就不要调优

    • 使用JMX、Prometheus等工具监控GC情况
    • 关注指标:GC频率、停顿时间、内存使用率
  2. 渐进式调整:一次只改一个参数

    • 先调整堆大小
    • 再调整新生代比例
    • 最后调整高级参数
  3. 压力测试:调优后一定要测试

    • 使用JMeter等工具模拟真实流量
    • 对比调优前后的性能指标

来看一个监控示例:

// 技术栈:Java 11
// GC监控示例
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;

public class GCMonitor {
    public static void main(String[] args) {
        // 获取所有GC MXBean
        List<GarbageCollectorMXBean> gcBeans = 
            ManagementFactory.getGarbageCollectorMXBeans();
            
        // 定期打印GC信息
        while (true) {
            gcBeans.forEach(bean -> {
                System.out.printf("GC名称: %s\n", bean.getName());
                System.out.printf("收集次数: %d\n", bean.getCollectionCount());
                System.out.printf("收集时间: %d ms\n", bean.getCollectionTime());
                System.out.println("-------------------");
            });
            
            try {
                Thread.sleep(5000); // 每5秒打印一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

六、常见问题与解决方案

  1. OOM错误:内存不够用

    • 方案:增加堆内存,检查内存泄漏
  2. GC过于频繁:影响吞吐量

    • 方案:增大新生代大小,调整Survivor区比例
  3. 长时间停顿:用户体验差

    • 方案:换用低停顿回收器(G1/ZGC),减少堆大小
  4. 内存碎片:分配大对象失败

    • 方案:使用G1等压缩回收器

来看一个内存泄漏的排查示例:

// 技术栈:Java 11
// 内存泄漏排查示例
import java.util.HashMap;
import java.util.Map;

public class MemoryLeak {
    private static Map<Long, byte[]> cache = new HashMap<>();
    
    public static void main(String[] args) {
        // 模拟缓存不断增长
        long counter = 0;
        while (true) {
            // 每秒往缓存添加1MB数据
            cache.put(counter++, new byte[1024 * 1024]);
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 打印内存使用情况
            System.out.printf("缓存大小: %d MB\n", cache.size());
        }
    }
}

这个程序运行一段时间后必定OOM,因为缓存只增不减。解决方案是给缓存设置上限或实现淘汰策略。

七、不同场景的调优策略

  1. Web应用:侧重低延迟

    • 推荐G1或ZGC
    • 设置合理的MaxGCPauseMillis
  2. 批处理应用:侧重高吞吐

    • 推荐Parallel GC
    • 增大堆内存和并行线程数
  3. 大数据应用:大内存需求

    • 推荐G1或Shenandoah
    • 设置非常大的堆内存
  4. 微服务:资源受限

    • 推荐Serial GC或ZGC
    • 使用较小的堆内存

八、调优的注意事项

  1. 不要过度调优:默认配置已经不错
  2. 测试环境≠生产环境:一定要在生产验证
  3. 关注应用本身:优化代码比调GC更有效
  4. 记录变更:每次调优都要记录参数和效果
  5. 长期监控:系统负载变化可能需要重新调优

记住,GC调优不是一劳永逸的,随着应用发展和硬件升级,需要定期重新评估。

九、总结

垃圾回收调优就像给汽车做保养,需要:

  • 了解基本原理(知道发动机怎么工作)
  • 选择合适的工具(不同的机油和滤清器)
  • 定期检查和调整(根据车况调整)

好的调优应该:

  1. 让应用运行更流畅
  2. 减少不必要的停顿
  3. 合理利用内存资源
  4. 适应业务需求变化

最后提醒,调优前一定要:

  • 做好监控
  • 了解应用特点
  • 小步调整
  • 充分测试