深入理解 JVM 垃圾回收:从分代模型到 G1 调优实战

一、为什么要理解 GC

Java 程序员不需要手动释放内存,但一旦出现 Full GC 频繁、STW 时间过长的问题,如果不理解垃圾回收原理,就只能靠猜。
一次典型的故障:某订单服务在大促期间 RT 从 50ms 飙升到 3s,监控显示每分钟触发 2~3 次 Full GC。

二、分代假说

JVM 基于两个分代假说设计堆结构:
  1. 弱分代假说:绝大多数对象都是朝生夕死的
  2. 强分代假说:熬过越多次 GC 的对象越难消亡
因此堆被划分为新生代(Eden + Survivor)与老年代,分别采用不同的回收算法。
Java
// 查看当前 JVM 使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version

// JDK8 默认:Parallel Scavenge + Parallel Old
// JDK17 默认:G1

三、四种收集器对比

| 收集器 | 算法 | 线程 | STW | 适用场景 | | --- | --- | --- | --- | --- | | Serial | 复制/标记整理 | 单线程 | 长 | 客户端、小内存 | | Parallel | 复制/标记整理 | 多线程 | 长 | 吞吐量优先 | | CMS | 标记清除 | 并发 | 短 | 低延迟(已废弃)| | G1 | Region + 标记整理 | 并发 | 可控 | 大堆、低延迟 |

四、判断对象存活

主流使用可达性分析而非引用计数,GC Roots 包括:
  • 虚拟机栈中引用的对象
  • 方法区静态属性、常量引用的对象
  • JNI 引用的对象
Java
public class GCRootsDemo {
    private static Object staticObj = new Object(); // 静态属性:GC Root
    public void method() {
        Object local = new Object(); // 局部变量:GC Root
        // 方法结束后 local 不再可达
    }
}

五、G1 调优实战

针对开篇的故障,最终参数如下:
Bash
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log

关键改动:

  1. 固定堆大小:避免动态扩缩容带来的抖动
  2. IHOP 从 45 下调:提前启动并发标记,避免退化成 Full GC
  3. 暂停目标 200ms:G1 会自动调整年轻代大小来满足目标

六、效果

| 指标 | 调优前 | 调优后 | | --- | --- | --- | | Full GC 次数/小时 | 150 | 0 | | 平均 RT | 320ms | 48ms | | P99 RT | 3.1s | 210ms |

小结

GC 调优不是玄学,核心是减少对象晋升到老年代的速度并给并发标记留出足够时间。先通过 jstat 和 GC 日志定位问题,再针对性调参,切忌直接抄网上的"万能参数"。
打赏作者 已有 0 人打赏,共 ¥0.00
我的打赏
Java架构沉思录
Java架构沉思录
Lv8 普通VIP 原创 8 粉丝 545

十年 Java 后端老兵,专注高并发与分布式架构

  • 8文章
  • 14.3万总阅读
  • 6597获赞
  • 545粉丝

评论(3)

编程小白鸭・澳大利亚
讲得太清楚了!尤其是 G1 调优那部分,我们项目正好遇到类似问题,明天就去试试。
1个月前 👍 41 回复 举报
运维老王・澳大利亚
马克一下,GC 日志分析这块能不能再出一篇?
1个月前 👍 28 回复 举报
Java架构沉思录・澳大利亚
感谢支持,下一篇就写 GC 日志怎么看。
1个月前 👍 25 回复 举报