引言
我所经历的大部分是 Java 应用, Java 语言的一个特性就是不需要使用者管理内存,Java 语言提供了管理内存的利器=》垃圾回收器。垃圾收集器虽然解决了使用者避免手动释放内存的问题,但是也给运行中的 Java 程序带来了一些影响:比如垃圾收集器在工作的时候不可避免的暂停应用程序的工作 即 STW 。STW 的耗时肯定是越低越好,这也是 GC 技术发展着重解决的问题。
概念
Garbage First 可以翻译成垃圾优先,这个就是 Garbage First 的核心要义:就是优先回收垃圾多的分区(region)。
Region 是 G1 不同于之前收集器的一个地方,它把整个堆分成了很多块,取名叫分区(region), 它在一次回收的时候为了降低 STW 的时间会选取一部分分区收集,这样就降低了耗时。分区的大小如果不指定是动态的, 可以指定大小:-XX:G1HeapRegionSize 设定,取值范围从 1M 到 32M, 基本上 16g 以下就 1m 或者 2m 即可。

Remember Set 可以翻译成“记住(得)某样东西的集合”,这里的某样东西就是:其他
分区 里对象 对于当前这个 分区 里对象的引用。
Remember Set 是 G1 起才有的,为什么会有这个东西呢,就是为了可以单独回收某个 分区,下面来看看这种场景:

假设堆里有两个分区 region A 和 region B, 现在选择回收 region B ,根据 三色标记法 由于不扫描 region A 内的对象,最后 objB 会被判定为 垃圾 被回收掉;那怎么才能知道 objB 被引用呢,记录一下谁引用了 objB 就可以了,remember set 就是干这个事的。
GC 流程
视角1:按 GC 类型
G1 也是分代的,只不过每个代的内存不是连续的,分为年轻代和年老代,大部分对象朝生夕死从出生到死亡都会在年轻代,一些存活较长的对象会晋升到年老代。
Young GC
年轻代采用的是标记-复制算法,先标记出哪些对象存活,然后统一复制到 幸存区2 , 幸存区2不够会放到年老代。。。
触发时机
- Eden 空间分配不足
2025-07-18T11:01:54.654+0800: 57280.455: [GC pause (G1 Evacuation Pause) (young), 0.0350196 secs]
[Parallel Time: 31.0 ms, GC Workers: 2]
[GC Worker Start (ms): Min: 57280455.8, Avg: 57280455.8, Max: 57280455.8, Diff: 0.0]
[Ext Root Scanning (ms): Min: 6.3, Avg: 6.4, Max: 6.6, Diff: 0.3, Sum: 12.8]
[Update RS (ms): Min: 6.3, Avg: 6.3, Max: 6.3, Diff: 0.0, Sum: 12.6]
[Processed Buffers: Min: 141, Avg: 143.5, Max: 146, Diff: 5, Sum: 287]
[Scan RS (ms): Min: 6.1, Avg: 6.2, Max: 6.3, Diff: 0.1, Sum: 12.4]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.4, Max: 0.7, Diff: 0.6, Sum: 0.8]
[Object Copy (ms): Min: 11.1, Avg: 11.6, Max: 12.1, Diff: 1.0, Sum: 23.1]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.1]
[GC Worker Total (ms): Min: 30.9, Avg: 30.9, Max: 31.0, Diff: 0.0, Sum: 61.9]
[GC Worker End (ms): Min: 57280486.7, Avg: 57280486.7, Max: 57280486.7, Diff: 0.0]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.9 ms]
[Other: 3.0 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.6 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.7 ms]
[Free CSet: 1.0 ms]
[Eden: 1582.0M(1582.0M)->0.0B(1584.0M) Survivors: 18432.0K->16384.0K Heap: 2442.8M(4000.0M)->284.6M(4000.0M)]
- G1 Humongous Allocation 大对象(超过 region size 一半的对象)会分配到年老代,如果年老代没有足够连续 region 分配会进行一次 young gc
2025-07-18T11:08:06.884+0800: 57652.685: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0728714 secs]
[Parallel Time: 64.2 ms, GC Workers: 2]
[GC Worker Start (ms): Min: 57652690.1, Avg: 57652690.1, Max: 57652690.2, Diff: 0.0]
[Ext Root Scanning (ms): Min: 8.5, Avg: 8.6, Max: 8.7, Diff: 0.2, Sum: 17.2]
[Update RS (ms): Min: 34.6, Avg: 36.0, Max: 37.4, Diff: 2.8, Sum: 72.1]
[Processed Buffers: Min: 903, Avg: 920.5, Max: 938, Diff: 35, Sum: 1841]
[Scan RS (ms): Min: 4.8, Avg: 6.2, Max: 7.6, Diff: 2.8, Sum: 12.4]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.4, Max: 0.8, Diff: 0.8, Sum: 0.8]
[Object Copy (ms): Min: 11.6, Avg: 12.1, Max: 12.5, Diff: 0.9, Sum: 24.2]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
[GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 63.4, Avg: 63.4, Max: 63.5, Diff: 0.1, Sum: 126.9]
[GC Worker End (ms): Min: 57652753.5, Avg: 57652753.6, Max: 57652753.6, Diff: 0.1]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.8 ms]
[Other: 7.8 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.7 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 1.6 ms] => 检查 humongous 对象是否已经不可达。及时释放这些占用的连续 old region。
[Free CSet: 0.8 ms]
[Eden: 1314.0M(1584.0M)->0.0B(1582.0M) Survivors: 16384.0K->18432.0K Heap: 2941.6M(4000.0M)->286.2M(4000.0M)]
[Times: user=0.14 sys=0.00, real=0.08 secs]
问:为啥 巨型对象分配在年老代,年老代没有连续空间要触发young gc , young gc 不是只回收年轻代的对象嘛?
Gpt 回答
晋升(promotion)
在 young GC 时:
eden 和 survivor 中存活的对象会晋升(promote)到 old region。
因此:
在 young GC 前,JVM 可能因为 survivor 空间不足,导致对象提前晋升(allocation failure fast path),产生碎片。
在 young GC 过程中,JVM 重新安排这些晋升:
将这些对象批量移动到 old region 中连续的 region。
因为 GC workers 在晋升过程中可以 合并多个散列分配请求,减少 region 之间的碎片。
另一原因:humongous reclaim
在 young GC 的 Other phase 中,会做:
Humongous Reclaim:
检查 humongous 对象是否已经不可达。
及时释放这些占用的连续 old region。
(GC log 中 [Humongous Reclaim: x.x ms] 说明 reclaim 了多少)
标记
标记用到的就是上面提到的三色标记法,这里简单用图示意下:


问题:如何解决年老代中的对象引用年轻代的对象的问题?
Remember set 见上文
所以年轻代 GC 标记时的根集合是 普通意义上的Gc Roots + 年轻代中各分区的 remember set
复制
复制就比较简单了略过
Mixed GC
Mixed GC 即混合式 GC, 这是 G1 的核心创新之一,这里的混合式既收集年轻代内存也回收年老代内存。
只不过为了减少 GC 的时间:年轻代全量回收,年老代回收部分 region (垃圾占比高的优先)
执行流程
Concurrent Mark
[1] Initial Mark (STW)
- 与 Young GC 合并
- 标记 GC Roots 直接可达对象
[2] Concurrent Mark (并发)
- 遍历全堆,统计各 Region Live Data Size
[3] Remark (STW)
- 捕获并发期间发生引用变动的对象
[4] Cleanup
- 计算可回收 Region
- 选择哪些 Old Region 加入 Collection Set (要收集 region 的集合)
上面是 G1 里的并发标记阶段和 CMS 里流程类似,这里 G1 取了个巧,初始标记的时候和 Young GC STW 重合在一起了,这样就减少了一次 STW 。
[5] Mixed GC 回收 (STW) (多次)
- 类似 Young GC,但 Collection Set 中包含了 Old Region
- 从 GC Roots + Remembered Set 标记可达对象
- 回收所有 Unreachable 对象
Full GC
full gc 是 Stop-The-World (STW) 的 全堆回收 ,会暂停所有应用线程,使用 单线程的标记-整理(Mark-Compact)算法,最后会会整理内存碎片,将对象移动,压缩堆空间。
未完待续。。。。