GC 原理

1. GC 日志与常用参数

在分析 GC 行为时,最直观的方法是查看 GC 日志。常见的参数组合如下:

bash复制编辑-verbose:gc 
-XX:HeapDumpPath=. 
-Xloggc:gc.log 
-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+UseGCLogFileRotation 
-XX:+DisableExplicitGC 
-XX:+PrintTenuringDistribution
  • -verbose:gc:打印简要的 GC 信息

  • -Xloggc:gc.log:指定 GC 日志输出文件路径

  • -XX:+PrintGCDetails:打印详细的 GC 信息

  • -XX:+PrintGCDateStamps:在日志中输出时间戳

  • -XX:+DisableExplicitGC:忽略显式调用 System.gc()

  • -XX:+PrintTenuringDistribution:打印对象晋升(Tenuring)阈值分布信息

  • -XX:HeapDumpPath:指定发生 OOM 时生成 heap dump 的路径

  • -XX:+UseGCLogFileRotation:开启 GC 日志轮转功能(需结合 -Xloggc 一起使用)

通过观察 GC 日志,可以分析 GC 触发原因对象存活率Minor/Full GC 次数与时间晋升阈值 等,从而制定或调整相应的 GC 策略和参数。


2. 对象分配机制

2.1 对象优先分配在 Eden 区

大多数情况下,使用 new 创建的新对象会被分配到 新生代(Young Generation) 中的 Eden 区。当 Eden 区满时,会触发一次 Minor GC,回收不再使用的对象,存活下来的对象会被移动(或复制)到 Survivor 区(通常是 s0s1)。

2.2 大对象直接进入老年代

如果有大对象(如超大数组)需要分配,而 Eden 区(包括 Survivor)即使经过 Minor GC 也无法容纳,则该对象会直接分配到 老年代(Old Generation)

  • 可通过参数 -XX:PretenureSizeThreshold(如 3M)来指定大对象阈值,大于该值的对象直接进入老年代。


3. 对象晋升规则

3.1 年龄阈值(Tenuring Threshold)

  • 新生对象在 Eden 中,年龄为 0

  • 每经历一次 Minor GC,存活对象的年龄 age++

  • 当年龄超过 -XX:MaxTenuringThreshold(默认 15)时,便会晋升到老年代。

  • 这一过程可以通过 -XX:+PrintTenuringDistribution 参数在日志中查看各年龄段对象分布。

3.2 动态年龄晋升

  • 为了在新生代腾出更多空间给新对象,JVM 会在 Minor GC 阶段中进行统计:

    • 如果某一年龄段的所有对象大小总和已经超过 Survivor 区的一半(或一定比例),则该年龄及以上的对象直接晋升到老年代。

  • 这种机制可以避免大量对象在 Survivor 区重复“挤占”空间,从而提高内存利用率


4. 生死抉择:可达性分析

4.1 GC Roots 可达性分析

判断对象是否存活,JVM 采用 可达性分析(Reachability Analysis)而非简单的引用计数:

  • GC Roots 通常包含:

    1. 虚拟机栈(栈帧)中的本地变量引用

    2. 方法区中类静态属性引用的对象

    3. 方法区中常量引用的对象(如字符串常量)

    4. 本地方法栈中 JNI 的引用

  • 从这些 GC Roots 出发,进行“图遍历”,能被遍历到的对象是“可达对象”,否则判定为“无用对象”。

4.2 引用计数法(已被淘汰)

  • 早期一些语言(如部分 C/C++ 库、Python)用引用计数判断对象存活,但存在无法解决循环引用的问题,Java 默认使用可达性分析来决定对象的去留。


5. 垃圾回收算法概述

垃圾回收算法本质上就是如何高效地回收无用对象并整理内存。在 JVM 中,最常见的有以下几类:

  1. 标记-清除(Mark-Sweep)

    • 标记阶段:从 GC Roots 出发,标记所有可达对象。

    • 清除阶段:遍历堆,把没有标记的对象回收。

    • 缺点:容易产生内存碎片,后续分配可能需要维护复杂的空闲列表。

  2. 标记-整理(Mark-Compact)

    • 在标记完成后,对存活对象做压缩整理,将其向一端移动,保证内存的连续性。

    • 避免了碎片问题,但对象移动开销较高。

  3. 复制算法(Copying)

    • 将内存分为两块(或多块)等大小区域,实际只使用其中的一块(From 区)。当该块用满时,将存活对象复制到另一块(To 区),然后清理 From 区。

    • 效率高且没有碎片,但实际可用内存被削减到一半。

    • 在新生代常用此算法,因为多数对象“朝生夕灭”,存活率低,复制成本也低。


6. 分代回收算法

Java 垃圾回收最大特征在于分代:将堆划分为 新生代老年代,以及方法区(Java 8 之后为 Metaspace)。

6.1 新生代

  • Eden + Survivor 0 + Survivor 1

  • 使用复制算法(Copying),在 Minor GC 时,从 Eden + 一块 Survivor 复制存活对象到另一块 Survivor,然后清空原 Survivor。

  • 存活率普遍较低,回收效率高且停顿时间短。

6.2 老年代

  • 对象从新生代晋升而来,存活时间较长或大对象直接放入。

  • 采用标记-清除标记-整理等算法。

  • 当老年代满或出现特殊情况(如调用 System.gc())时触发 Major GC / Full GC,停顿时间相对较长。

6.3 永久代(方法区 / Metaspace)

  • 早期 JVM 中的“永久代(PermGen)”存放类元数据、常量池、静态变量等。在 Java 8 之后,改为使用本地内存的 Metaspace

  • 永久代/Metaspace 也可能发生回收,包括 废弃常量无用的类

    • 回收条件:

      1. 该类所有实例已经回收;

      2. 该类的 Class 对象没有被引用;

      3. 加载该类的 ClassLoader 已被回收;

    • -Xnoclassgc 可以禁止类卸载,适用于大量使用动态代理或 CGLib 的场景,以避免频繁的类加载卸载导致方法区溢出。


7. 分区收集算法(G1、ZGC 等)

随着大型内存应用的需求,出现了 G1ZGCShenandoah 等更先进的收集器,它们都采取某种形式的分区(Region)或分块管理方式。

7.1 分区思想

  • 将整堆划分为若干大小相等的 Region,既可以放新生代,也可以放老年代对象。

  • 回收时并行和并发地做标记,优先回收垃圾最多的 Region,减少一次性清理大块内存所需的时间,并控制停顿时间

7.2 G1(Garbage-First)

  • 经典的分区收集器,在 JDK 9+ 成为默认的服务端收集器。

  • 年轻代 GC(Young GC)混合 GC(Mixed GC) 两种模式,通过并发标记找出大量垃圾的 Region,集中回收。

  • 目标是可预测的低延迟,适合较大堆内存、对停顿敏感的应用。

7.3 ZGC / Shenandoah

  • 进一步降低 STW(Stop-The-World)时间,通过并发标记染色指针(colored pointers)读/写屏障等手段,实现超低延迟(如停顿时间不超过 10ms)

  • 更适合超大内存(如 TB 级别)场景。


8. 常用 JVM/GC 参数

  1. -Xms / -Xmx:设定 JVM 初始堆和最大堆大小

  2. -Xmn:设置新生代大小(有时也可以用 G1 的 -XX:NewRatio 进行调整)

  3. -XX:SurvivorRatio:Eden 与 Survivor 区大小比例

  4. -XX:MaxTenuringThreshold:最大晋升年龄阈值

  5. -XX:PretenureSizeThreshold:大对象直接进老年代

  6. -XX:MetaspaceSize / -XX:MaxMetaspaceSize:控制 Metaspace 大小(Java 8+)

  7. -XX:+UseConcMarkSweepGC / -XX:+UseG1GC / -XX:+UseZGC:选择垃圾收集器

  8. -XX:+PrintGCDetails / -Xloggc:输出详细 GC 日志

在实际生产环境中,需要结合业务吞吐量延迟内存大小等因素来选择和调优收集器,并合理设置各项参数。


9. 总结与优化思路

  • 分代思想:根据对象存活周期,把堆划分为新生代与老年代,让 GC 针对不同区域使用最合适的回收算法。

  • Minor GC 频繁但停顿短Full GC 较少但停顿长;尽量减少 Full GC 发生。

  • 大对象直接进入老年代,避免在新生代中频繁复制。

  • 晋升规则主要包括年龄阈值与动态年龄判断,以保证 Survivor 区的可用性。

  • 可达性分析判断对象生死,标记-清除/复制/标记-整理等多种算法相结合,在不同区域/收集器中使用。

  • 分区收集(G1 / ZGC / Shenandoah) 是未来趋势,适合大堆内存、低延迟需求。

  • GC 参数调优 需基于 GC 日志性能监控 进行针对性调整,切忌盲目使用默认或某些“通用”参数。

通过充分理解以上原理和机制,结合实际应用场景的需求(如吞吐量、延迟、内存使用等),才能对 GC 行为进行更加合理有效的优化,从而提升应用的稳定性和整体性能

Last updated

Was this helpful?