G1 GC 将堆划分为多个大小相等的 Region,在每次年轻代或混合回收时,并不会一次性清理整个堆,而是选出一部分 Region 组成 Collection Set(简称 CSet)进行回收。CSet 的选择算法直接决定了一次停顿能回收多少垃圾,以及是否会超出我们设定的停顿时间目标。要分析它是如何平衡的,需要从回收价值和停顿模型两个角度入手。

什么是 CSet 与回收价值
CSet 是一次 GC 停顿中将被回收的 Region 集合。G1 会为每个 Region 估算两个指标:可回收空间大小,以及回收该 Region 所需的预计耗时(包括扫描根、拷贝存活对象等成本)。回收价值通常用“单位时间能回收的空间”来衡量,也就是性价比。
年轻代与混合回收的区别
年轻代回收时,所有 Eden 和 Survivor Region 都会被强制加入 CSet,因为年轻代对象死亡率高、回收价值明确。而混合回收(Mixed GC)阶段,老年代 Region 是否进入 CSet 就看算法权衡了。
CSet 选择如何受限停停顿时间
G1 根据用户配置的 MaxGCPauseMillis 作为软目标。在选择老年代 Region 进入 CSet 时,算法会按回收价值从高到低排序,然后依次累加预计停顿时间,直到达到目标上限或没有更高价值的 Region 可选。
| Region | 可回收空间(MB) | 预计耗时(ms) | 回收价值 |
|---|---|---|---|
| R1 | 50 | 10 | 5.0 |
| R2 | 30 | 20 | 1.5 |
| R3 | 80 | 40 | 2.0 |
假设停顿目标为 50ms,优先选 R1(10ms),再加 R3(40ms)共 50ms 达标,R2 因价值低且会超时则不被选中。
如何通过日志分析 CSet 决策
开启 G1 的详细日志(如 -Xlog:gc*=debug)能看到 CSet 的构成与耗时。我们可以关注以下字段:
- Eden/Survivor regions count:年轻代 CSet 规模
- Old regions count:混合回收选入的老年代 Region 数
- Predicted pause:预测停顿 vs 实际停顿
示例:打印 G1 相关参数
下面这段 Java 启动参数可以帮助我们获取分析所需数据:
# 设置停顿目标并开启 G1 调试日志
java -XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc*=debug:file=gc.log
-jar app.jar
用代码模拟简单的价值排序选择
为理解选择逻辑,可用简单程序模拟按价值排序并受停顿约束的 CSet 构建:
import java.util.*;
class Region {
String name;
int reclaimMB;
int costMs;
double value() { return (double) reclaimMB / costMs; }
}
public class CSetSim {
public static void main(String[] args) {
List<Region> regions = new ArrayList<>();
// 构造示例 Region
regions.add(mk("R1", 50, 10));
regions.add(mk("R2", 30, 20));
regions.add(mk("R3", 80, 40));
// 按回收价值降序
regions.sort((a, b) -> Double.compare(b.value(), a.value()));
int pauseTarget = 50;
int used = 0;
List<Region> cset = new ArrayList<>();
for (Region r : regions) {
if (used + r.costMs <= pauseTarget) {
cset.add(r);
used += r.costMs;
}
}
System.out.println("CSet: " + cset.stream().map(x -> x.name).toList());
}
static Region mk(String n, int m, int c) {
Region r = new Region();
r.name = n; r.reclaimMB = m; r.costMs = c;
return r;
}
}
调优时的注意点
如果观察到实际停顿经常高于目标,可能是 Region 回收成本估算偏差,或堆内存活对象过多导致拷贝耗时上升。此时应结合 CSet 日志看是否选入了低价值大耗时 Region,并适当降低期望或调整 InitiatingHeapOccupancyPercent 以更早启动混合回收。
G1 的 CSet 选择本质是一个带时间约束的贪心算法:在停顿预算内尽可能多捞高回收价值 Region。
理解这套机制后,我们再读 GC 日志就不会只盯着重填次数,而能明白为什么某次混合回收只清了很少的老年代,从而做出更有效的参数调整。