G1、ZGC 与收集器取舍:区域、周期与停顿证据
本页主线固定为 OpenJDK
jdk-17.0.12+7,实验运行 Oracle JDK 17.0.12(macOS x86_64,8 核),JDK 21/23/24 只用对应 JEP 解释代际 ZGC 的变化。规范与实现、预期与实测分三层表述;停顿数字属于测量结果,不是收集器承诺。
Region 组织、Humongous 与停顿工作量
G1 把连续堆划成等大的 region,每个 region 可承担 Eden、Survivor、Old、Humongous 等角色。年轻代不必是单块连续空间;一次年轻回收的 Collection Set 包含本轮年轻区域,Mixed 再加入部分老区。回收存活对象需要有目标空间,所以“本轮有很多垃圾”不代表疏散一定能成功(JDK 17 G1 指南)。
局部回收必须知道区外指向区内的引用。卡表以固定粒度记录引用字段可能被修改的地址范围,RSet 则帮助某个 region 找到外部引用来源;卡不等于单个对象,也不是精确的全部边集合。业务写引用后产生脏卡,并发 refinement 将其加工为后续扫描信息;如果积压,停顿中就可能付出额外扫描成本。大量跨区引用与高更新率会增大 RSet 内存和扫描工作,即使存活对象不多也能拖长暂停。SATB 解决并发标记的旧边丢失,后写屏障解决局部回收的外部引用定位,两者不能替代(g1BarrierSet.inline.hpp 的前/后写屏障)。
源码阅读可沿 G1CollectedHeap::do_collection_pause_at_safepoint 看 STW 疏散主流程,并结合 G1Policy 看暂停预算、年轻代尺寸与老区候选选择。大致链路是建立本轮收集集、处理根和记忆集、疏散对象、引用修正、回收/清理 region 以及更新策略。政策层的预测只能估算,不知道下一次对象图与 CPU 调度一定如何变化;MaxGCPauseMillis 因此是软目标,设成 10 ms 不能证明每次都低于 10 ms(g1CollectedHeap.cpp、g1Policy.cpp)。
Humongous 对象直接占用连续 region,判定阈值与 region 大小、对象头和对齐有关。固定 tag 中 G1CollectedHeap::is_humongous 按 word size 与半 region 阈值比较,普通教学可说“大于半 region 的对象”,边界必须包含对象头、对齐后再查实现。例如 region=1 MiB,数组有效负载约 1 MiB 时,加上头部后可能占两个 region;不能按 payload 刚好 1 MiB 推断只占一个。大量空闲 region 分散也不保证能满足连续需求。
原码:is_humongous 的严格大于判断
原码连续摘录(仅统一公共缩进,截取方法整体):g1CollectedHeap.hpp,jdk-17.0.12+7,L1298–L1306。
// Returns "true" iff the given word_size is "very large".
static bool is_humongous(size_t word_size) {
// Note this has to be strictly greater-than as the TLABs
// are capped at the humongous threshold and we want to
// ensure that we don't try to allocate a TLAB as
// humongous and that we don't allocate a humongous
// object in a TLAB.
return word_size > _humongous_object_threshold_in_words;
}注释直接给出“严格大于”的原因:TLAB 上限正好等于该阈值,用 > 才能保证 TLAB 本身不被判为 Humongous,也不会在 TLAB 里分配 Humongous 对象。判定的输入是 word size 而不是 payload 字节数,头部与对齐已经包含在内;阈值 _humongous_object_threshold_in_words 随 region 大小变化。这一段只回答“多大算 Humongous”,回收时机是另一个问题——Humongous 可能在年轻回收中被积极回收,不必一概等到 Full GC,具体受对象种类和引用跟踪等条件影响。
修复方向也由此确定:不要将“Region 总空闲字节充足”等同于“任何分配都可成功”;候选方案是分块流式处理、避免中间大数组、给批次设上限,或者在测量后调整 region。增大 region 会抬高 Humongous 门槛,也会降低局部回收粒度,不能只看一次日志的 Humongous 数下降。
正常周期、疏散失败与容量算例
正常情况下 Young-only 期间由 Concurrent Start 启动标记,SATB 并发确定老区存活量,Remark 最终收尾,Cleanup 完成候选决策,之后 Prepare Mixed/Mixed 回收部分老区。不是每次 Young 都混合回收,也不是 Concurrent Mark 结束就把所有老年代垃圾搬走。完整三色推演、图与历史实测见 GC Roots 单元。
复制对象时两个 GC worker 可能发现同一对象,不能各自生成一个语义不同的最终副本;实现通过 forwarding 协议协调,失败者使用已发布的转发结果。目标空间不足时进入 evacuation failure 分支,部分对象无法搬迁,需保留并修复状态,后续可能走 Full GC。看到 To-space exhausted 应把本轮存活量、可用 region、目标空间余量和并发周期是否滞后放在一起看,而非直接增加 Young 大小——更大 Young 可能反而增加单次复制量。
原码:复制快路径与疏散失败入口
原码连续摘录(仅统一公共缩进,截取方法前段):g1ParScanThreadState.cpp,jdk-17.0.12+7,L418–L445。
oop G1ParScanThreadState::do_copy_to_survivor_space(G1HeapRegionAttr const region_attr,
oop const old,
markWord const old_mark) {
assert(region_attr.is_in_cset(),
"Unexpected region attr type: %s", region_attr.get_type_str());
// Get the klass once. We'll need it again later, and this avoids
// re-decoding when it's compressed.
Klass* klass = old->klass();
const size_t word_sz = old->size_given_klass(klass);
uint age = 0;
G1HeapRegionAttr dest_attr = next_region_attr(region_attr, old_mark, age);
HeapRegion* const from_region = _g1h->heap_region_containing(old);
uint node_index = from_region->node_index();
HeapWord* obj_ptr = _plab_allocator->plab_allocate(dest_attr, word_sz, node_index);
// PLAB allocations should succeed most of the time, so we'll
// normally check against NULL once and that's it.
if (obj_ptr == NULL) {
obj_ptr = allocate_copy_slow(&dest_attr, old, word_sz, age, node_index);
if (obj_ptr == NULL) {
// This will either forward-to-self, or detect that someone else has
// installed a forwarding pointer.
return handle_evacuation_failure_par(old, old_mark);
}
}方法前段展示了三条信息:目标 region 属性由 next_region_attr(年龄、是否为 Survivor)决定;分配先走线程本地 PLAB 快路径,注释明确“大多数情况一次判空即可”;PLAB 失败才进入 allocate_copy_slow,再失败才落到 handle_evacuation_failure_par。最后一行的注释就是 forwarding 协议:要么把对象转发给自己(保留原地),要么发现别的 worker 已装好转发指针,使用对方的结论。把“疏散失败”理解成单个对象复制出错会解释错——它是一条全局分支,失败数量累积会影响本轮回收与后续 Full GC 决策。
IHOP 源码链是 G1Policy::need_to_start_conc_mark 取 non_young_capacity_bytes 加预测请求,与控制器阈值比较;record_new_heap_size 按当前堆 region 数更新 target。固定控制器按百分比乘目标堆容量,自适应控制器再考虑老年代分配率、标记时间、年轻代与预留空间。因而要区分“比较的是非年轻代占用”和“百分比的容量基数”,不要直接用老年代已使用量相乘(g1IHOPControl.hpp)。
简化容量推演:当前可供周期增长的余量 600 MiB,老年代净增长 200 MiB/s,距离第一次 Mixed 约 2 s,预计至少增长 400 MiB,再加复制/波动余量 250 MiB,合计 650 MiB,已经超过 600。更早启动能缓解,但若 CPU 节流使周期变成 4 s,则需要 1050 MiB 左右;调百分比无法凭空创造容量,应减少晋升/大对象、给 GC CPU 或扩容。这是近似推演,不是 IHOP 实际源码公式。
图解:region 网格与一轮回收周期
图左侧把堆画成等大 region 网格,颜色区分 Eden、Survivor、Old 与 Humongous;注意 Humongous 占连续格子,Mixed 回收时只会挑走一部分 Old 格子。右侧把一轮周期排成七步,红字为 STW、绿字为并发:真正停顿的是各次 Young、Remark 与 Cleanup,并发标记穿插在其中。读图时把“哪些格子属于本轮 Collection Set”和“哪个阶段能决定回收哪些格子”分开看,它们分别对应疏散工作量与候选选择逻辑。
并行与并发正确性约束
并行收集表示多个 GC 工作线程共同工作;并发收集表示 GC 工作与应用线程重叠。两者都可能在某些阶段停顿应用。低停顿是目标与测量结果,不是无停顿承诺。
并发标记期间,应用线程仍能修改引用图。收集器必须用写屏障、读屏障或其他记录结构维护“已标记对象仍覆盖正确存活集”等不变量。SATB 类方案记录并发周期开始后的旧引用,以保守保留快照开始时的存活对象;卡表等结构记录被修改区域,支持后续扫描。
屏障有 CPU、内存和缓存成本,且屏障形态与收集器和版本相关。漏掉存活对象是正确性错误,多保留对象通常体现为本轮回收效率下降。屏障不只是“多插一条指令”,而是与并发状态、区域跟踪和阶段转换共同构成协议。
JDK 17 ZGC:读屏障与阶段序列
JDK 17 ZGC 是非代际收集器,不套用 G1 Eden/Survivor/晋升语言。它用带元数据的引用表示和 load barrier,配合转发表,使大部分标记与重定位与业务并发;短暂停顿仍存在。ZDriver::gc 依次调度各阶段;关键分支是 while (!pause_mark_end()) concurrent(mark_continue):若还不能结束标记,继续并发处理后再尝试,不能把每个周期画成永不回头的直线。本机 1 GB 堆启动日志显示 Address Space Size: 16384M x 3 = 49152M(虚拟地址预留)与 GC Workers: 2 (dynamic)(实验机 8 核),说明 ZGC 的地址空间与并发工作线程属于独立预算项。
原码:ZGC 读引用先经过快慢路径分流
原码连续摘录(仅统一公共缩进,截取方法局部):zBarrier.inline.hpp,jdk-17.0.12+7,L230–L237。
inline oop ZBarrier::load_barrier_on_oop_field(volatile oop* p) {
const oop o = Atomic::load(p);
return load_barrier_on_oop_field_preloaded(p, o);
}
inline oop ZBarrier::load_barrier_on_oop_field_preloaded(volatile oop* p, oop o) {
return barrier<is_good_or_null_fast_path, load_barrier_on_oop_slow_path>(p, o);
}先原子读取引用,再传给 preloaded 入口;模板参数同时提供 is_good_or_null_fast_path 与 load_barrier_on_oop_slow_path。不是每次读都做昂贵修复,慢路径职责也不能从这个入口凭空推出,需继续追目标函数及重定位状态。该图景限定 JDK 17 非代际 ZGC,不直接套用新版本代际屏障。
引用读取的实际链可沿 load_barrier_on_oop_field → load_barrier_on_oop_field_preloaded → 模板 barrier → load_barrier_on_oop_slow_path 阅读。fast path 接受当前 good/null 引用;slow path 按当前标记/重定位阶段处理并返回可用地址。对有地址的字段,随后尝试 self-heal,减少下次同类慢路径(zBarrier.inline.hpp、zBarrier.cpp)。self_heal 不是无条件写回:不以 null 修复以免破坏引用处理;CAS 成功结束,若别的屏障已经留下满足 fast path 的值则返回,否则在协议约束内继续升级引用状态。GC worker 与应用可能同时访问搬迁中的对象,转发表与屏障共同协调,不需要先扫描并立刻改好全堆每个引用再恢复应用。“ZGC 不 STW”和“所有引用读取都无额外成本”都不成立。
原码:ZDriver::gc 的阶段序列
原码连续摘录(仅统一公共缩进,截取方法整体):zDriver.cpp,jdk-17.0.12+7,L445–L480。
void ZDriver::gc(const ZDriverRequest& request) {
ZDriverGCScope scope(request);
// Phase 1: Pause Mark Start
pause_mark_start();
// Phase 2: Concurrent Mark
concurrent(mark);
// Phase 3: Pause Mark End
while (!pause_mark_end()) {
// Phase 3.5: Concurrent Mark Continue
concurrent(mark_continue);
}
// Phase 4: Concurrent Mark Free
concurrent(mark_free);
// Phase 5: Concurrent Process Non-Strong References
concurrent(process_non_strong_references);
// Phase 6: Concurrent Reset Relocation Set
concurrent(reset_relocation_set);
// Phase 7: Pause Verify
pause_verify();
// Phase 8: Concurrent Select Relocation Set
concurrent(select_relocation_set);
// Phase 9: Pause Relocate Start
pause_relocate_start();
// Phase 10: Concurrent Relocate
concurrent(relocate);
}暂停阶段是 Phase 1 Pause Mark Start、Phase 3 Pause Mark End(可带 3.5 并发续跑循环)、Phase 7 Pause Verify(诊断用)与 Phase 9 Pause Relocate Start;其余标记、非强引用处理、重定位集合选择与搬迁主体都是 concurrent。Phase 3 的 while 结构再次说明“标记结束”不是一次性事件:并发阶段追不上时允许继续并发,再尝试收口。对照实验日志:本机 1 GB 堆 10 s 的 -Xlog:gc* 轮次里 100 个周期,三类暂停各 100 次——Mark Start mean 0.019 ms / max 0.079 ms,Mark End mean 0.021 ms / max 0.061 ms,Relocate Start mean 0.019 ms / max 0.208 ms;全周期暂停最大 0.208 ms。
低 GC 停顿仍可能出现 Allocation Stall:回收/重定位的供给追不上分配需求时,分配线程会等待。它不一定表现为单次长全局 GC pause,却同样影响业务 P99。因此同时看 GC pause、周期时长、分配 stall、CPU 与内存余量。SoftMaxHeapSize 是软目标,硬约束仍有 Xmx;设置很小的软目标不能保证 RSS 永远小于该值(JDK 17 ZGC 指南)。
代际 ZGC 的版本变化必须单独回答
| 版本 | 选择与变化 | 不能倒套的结论 |
|---|---|---|
| JDK 17 | -XX:+UseZGC,非代际 | 没有 JDK 21 的 ZGenerational 代际选择 |
| JDK 21 | JEP 439 引入代际,-XX:+UseZGC -XX:+ZGenerational 启用 | 默认 UseZGC 还不能直接等同代际 |
| JDK 23 | JEP 474 默认使用代际,非代际被弃用 | 默认行为相对 21 已改变 |
| JDK 24 | JEP 490 移除非代际实现 | 不能继续要求切回旧模式 |
代际 ZGC 区分年轻/老年代,引入相应写屏障及 remembered set 协议,让多数短命对象无需每轮全堆处理;引用表示、屏障职责也随设计变化。不能把 17 的 load-barrier-only 示意当作新实现的完整机制,更不能把 G1 的 SATB/card 细节直接套过去(JEP 439、JEP 474、JEP 490)。
收集器家族对照与选型决策记录
前面各节讲了 G1 与 ZGC 的机制;选型问题要把整个 JDK 17 家族放在同一张表上(Serial/Parallel 指南、G1 指南、ZGC 指南):
| 收集器 | 形态 | 适用面 | 主要代价与边界 | JDK 17 状态 |
|---|---|---|---|---|
| Serial(-XX:+UseSerialGC) | 单线程 STW | 单 vCPU/小容器/客户端 | 全程停顿,无并发能力 | 可用 |
| Parallel(-XX:+UseParallelGC) | 多线程 STW | 吞吐优先、可容忍秒级停顿的批处理 | 停顿长且不可控;无并发标记;存在晋升失败/担保语义(见GC 日志单元) | 可用 |
| G1(17 默认) | region + 并发标记 + STW 疏散 | 平衡停顿与吞吐的大堆通用服务 | 跨区引用/RSet 成本、Humongous 治理、Full GC 退化为整堆压缩(升级梯见GC 日志单元) | 默认 |
| ZGC(非代际,17) | 并发标记/重定位 + 读屏障 | 超大堆、亚毫秒停顿目标 | 并发工作抢 CPU、内存余量(地址预留三倍)、Allocation Stall;诊断生态较新 | -XX:+UseZGC |
| 代际 ZGC | 分代 + 新写屏障 | 同上且多数对象短命时更省 | JDK 21 才可显式启用(JEP 439),17 不可用 | 不可用 |
对照表的用法不是背"谁最好",而是排除:单 vCPU 配额下并发收集器(G1 的并发标记、ZGC 全套)会与业务抢 CPU,Serial 反而可能是正解;批处理允许 2 秒停顿时 Parallel 的吞吐优势直接成立;P99 要求 50 ms 且堆 32 GiB 以上,G1 的 Mixed 停顿开始吃力,才进入 ZGC 的论证区。每个"适用"都要用本页实验章的同负载对照法验证,不拿表格当结论。
选型决策记录模板与容量预算算例
一次可审计的选型决策至少记录六项(缺一会被追问打回):基线证据(当前收集器在同到达率下的停顿分布/GC CPU 占比/回收后基线)、候选对照(同负载同版本实测,不是厂商数字)、资源预算(内存余量、GC 线程 CPU 配额、地址空间)、失败模式(G1 的 To-space exhausted 与 Full GC 升级、ZGC 的 Allocation Stall——本页 256 MB 堆实测 347 次 stall 就是直接证据)、灰度与回滚阈值(P99/错误率/CPU 触发线)、验收窗口(N 小时稳定 + 相同到达率复测)。
容量预算算例(纸面推演,非实测,方法沿用本页 IHOP 算例的口径):8 GiB 堆、稳定存活 4 GiB、分配峰值 1 GiB/s,比较两个候选的"必须额外容纳量"。G1:并发标记周期 2 s 期间新增分配约 2 GiB + 复制目标空间(Mixed 每轮搬 1/8 老年代约 0.5 GiB)+ 浮动垃圾余量,合计需 ~2.5–3 GiB 机动空间,8 GiB 堆下占用率长期高于 85%,IHOP 会被反复触发——预算偏紧。ZGC(17 非代际):每周期全堆标记+重定位,同样 2 s 周期需容纳 2 GiB 新分配,另需重定位转发表与染色指针元数据(内存开销小但地址预留大),并发线程在 4 vCPU 下占用约 1 个核——CPU 预算要扣,内存余量反而宽松。结论形态:"该负载下 G1 内存预算紧、ZGC CPU 预算紧",再按实例实际配额选边并用上面六项记录。若条件改为 1 vCPU:ZGC 的并发线程占比过高,G1 关并发周期(或 Serial)进入候选;改为 64 GiB 堆 + P99 50 ms:G1 Mixed 单轮停顿随堆增长,ZGC 优势扩大。这些数字是用乘除法从假设推出来的预算形态,不是任何机器的实测结果;真实决策必须替换成同负载实测值。
安全点、停顿解释与证据边界
虚拟机需要在能安全观察线程状态的位置执行部分全局操作。安全点要求线程处于可检查状态;较久不轮询的执行路径或 OS 调度延迟可能拉长同步。线程处于可安全检查的 native 状态时,不是必须等 native 调用结束才能 GC,返回 Java 时要通过状态转换检查;JNI critical 与 GC locker 等约束需单独分析。不能把所有长 native 调用都归为“阻止安全点”。
开启 -Xlog:gc*,safepoint,看 reaching safepoint、cleanup、at safepoint 和 total 等实际字段。某次 300 ms 延迟若主要花在 reaching,就不能归因于复制了太多存活对象;如果 at safepoint 很高,再看 GC phase。一次停顿总时长需分解为请求同步、根扫描、对象处理、复制或清理等工作;日志报告的停顿时间不等价于整个请求端到端等待,操作系统调度、容器节流、锁等待和下游阻塞需从其他遥测验证(safepoint.cpp)。
即使停顿只有 5 ms,也可能造成请求积压:假设服务原来 90% 利用率,暂停期间排入的请求要靠仅 10% 空余处理能力消化,恢复时间可能远大于 5 ms。实际多实例、批处理与重试会改变结果,所以同时看到达率、队列和业务延迟。Java RUNNABLE 也可能在 native I/O 等待,不等于正在获得 CPU。
可运行对照:只改变一个收集器条件
先按 实验 README 编译,lab_build 为输出目录,两个 JVM 顺序运行以免互相争抢 CPU:
lab_jdk=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home
lab_build=$(JVM_LAB_JAVA_HOME="$lab_jdk" bash labs/jvm-senior/build.sh)
lab_evidence=$(mktemp -d)
"$lab_jdk/bin/java" -Xms256m -Xmx256m -XX:+UseG1GC \
"-Xlog:gc*,safepoint:file=$lab_evidence/g1.log:time,uptime,level,tags" \
-cp "$lab_build" JvmSeniorLab churn 10
"$lab_jdk/bin/java" -Xms256m -Xmx256m -XX:+UseZGC \
"-Xlog:gc*,safepoint:file=$lab_evidence/zgc.log:time,uptime,level,tags" \
-cp "$lab_build" JvmSeniorLab churn 10必须先用 java -version 确认是 17,避免默认 PATH 使用 21。输入是一次保留一个 256 KiB 数组的短命分配循环,运行 10 s。预期 G1 能看到 Young evacuation,ZGC 能看到自身标记/重定位日志,并可能出现 Allocation Stall;不保证小堆能呈现所有失败分支。实测(本轮运行,G1 与 ZGC 各自独立启动):
# 256 MB 堆,churn 10
G1 :allocatedArrays=469190;1032 次 Pause Young(mean 1.058ms,max 18.177ms);
运行末期一次回收 153M->1M(256M)
ZGC:allocatedArrays=356749;426 个 GC 周期;347 次 Allocation Stall (main)
(mean 7.614ms,max 14.941ms);堆状态 250M(98%)->18M(7%)
# 1 GB 堆,churn 10
G1 :allocatedArrays=457007;250 次 Pause Young(mean 1.982ms,max 27.158ms)
ZGC:allocatedArrays=423361;143 个 GC 周期(119 次由 Allocation Rate 触发);
24 次 Allocation Stall(mean 7.579ms,max 11.954ms)对照读法:同样按时长运行,两个收集器的分配总量本来就不会相同,因此不比较“谁快”。可比较的是形态——G1 的代价落在 1032 次 Young 暂停里(尾部 max 18.177 ms),ZGC 的代价落在 347 次 Allocation Stall 与更少的周期里(单次 stall max 14.941 ms);堆加大到 1 GB 后两者都明显缓解(G1 250 次、ZGC 24 次 stall)。ZGC 减少的是 GC 暂停,不是分配等待;小堆下 stall 会直接表现为业务线程等待。正式性能比较须固定完成工作量或固定外部到达率,并报告错误率、CPU、吞吐及业务延迟。
原有 labs/gc 的 G1 marking 日志仍是并发周期的独立实测;U11 新增的 [labs/gc/diagnose.sh](<GC日志解读与线上故障诊断.md#可运行实验-四个受控入口) 用受控小堆触发 G1 的 Full GC 升级梯与 Humongous 原因链("后续可能走 Full GC"一句在那页有完整梯子实测)。要验证 evacuation failure,可先纸面缩减余量并推演,再在隔离环境提高保留集合;本页不提供可误杀生产服务的无界分配循环。ZGC 不支持的平台会在启动报错,应记录为环境限制,不能伪造成功日志。
高级答案与三轮追问
主问:“G1 与 ZGC 怎么选?”答:先固定 JDK 与负载,G1 用 region 和 STW 疏散平衡吞吐与暂停,17 ZGC 用并发标记/重定位和读屏障降低停顿,代价涉及 CPU、内存余量和诊断成熟度;按业务 P99、吞吐、CPU 配额和峰值内存验证。
第一轮:“G1 设 20 ms,为何仍 100 ms?”答:软目标,根扫描/RSet、存活复制、目标空间和 OS 调度都能超预测;MaxGCPauseMillis 只影响策略选择的工作量。评分点:把暂停同步和 GC 内部工作分开。
第二轮:“ZGC pause 小,为何接口仍慢?”答:查分配 stall、并发 GC 与业务抢 CPU、队列/下游和容器节流;GC pause 不能覆盖全部 wall time。实验里 256 MB 堆下 347 次 stall 就是直接证据。评分点:至少两类独立证据。
第三轮:“迁到 21 就算已验证代际 ZGC?”答:确认实际参数、日志与构建,21 默认 UseZGC 与显式代际选择不同,23/24 又变更;需同负载灰度和回滚条件。评分点:版本、运行证据和业务验收。
架构师变化条件与可执行决策
假设 8 GiB 堆、稳定存活 4 GiB、分配峰值 1 GiB/s,4 vCPU,接口 P99 目标 100 ms。A 用 G1,通过批次上限、缓存生命周期和减少跨区更新改善暂停;B 用 ZGC,为并发工作与重定位留内存/CPU。若最大可用额外空间只有 1 GiB,而一个周期需 2 s,粗略新增分配需求已达 2 GiB,不能只看暂停指标宣布 B 成功。需要测对象死亡/回收速度,不能把全部分配都当净增长,也不能假设全部立即可回收。
条件改为 1 vCPU quota,优先验证并发 GC 是否挤压业务,减少线程可能比增加 GC 线程更有效;条件改为 64 GiB 堆且要求更低尾延迟,则并发重定位的收益可能变大。决策记录至少给出基线、同到达率对照、容量余量、失败注入、灰度比例、触发回滚的 P99/错误率/CPU/内存阈值,阈值由实际 SLO 定义。专家深化可继续读 forwarding 原子协议,但不是本轮所有学习者的统一门槛。
闭卷验收
本页 12 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| Oracle JDK 17 G1 调优指南 | JDK 17 | region、Collection Set、停顿目标、周期阶段 |
| Oracle JDK 17 ZGC 指南 | JDK 17 | 17 ZGC 阶段、Allocation Stall、SoftMaxHeapSize |
| OpenJDK 17u g1CollectedHeap.hpp | jdk-17.0.12+7 | is_humongous 判定(L1298–L1306) |
| OpenJDK 17u g1ParScanThreadState.cpp | jdk-17.0.12+7 | PLAB 复制快路径与疏散失败入口(L418–L445) |
| OpenJDK 17u zDriver.cpp | jdk-17.0.12+7 | ZDriver::gc 阶段序列(L445–L480) |
| OpenJDK 17u zBarrier.inline.hpp | jdk-17.0.12+7 | 读屏障快慢路径分流(L230–L237)与 self-heal |
| OpenJDK 17u g1IHOPControl.hpp | jdk-17.0.12+7 | IHOP 触发口径与目标容量更新 |
| OpenJDK 17u safepoint.cpp | jdk-17.0.12+7 | safepoint 同步与停顿字段 |
| JEP 439 / JEP 474 / JEP 490 | JDK 21/23/24 | 代际 ZGC 的引入、默认化与旧实现移除 |
| JDK 17 可用收集器与并行指南 / 并行收集器指南 | JDK 17 | 家族对照表的 Serial/Parallel 侧依据(U11 补强段) |
本地 JvmSeniorLab churn 对照实验 | Oracle JDK 17.0.12 | G1/ZGC 同负载下的暂停与 stall 实测 |
访问日期:2026-10-23。源码行号以 jdk-17.0.12+7 标签为准;实测数字来自本机 Oracle JDK 17.0.12(macOS x86_64、8 核),更换 JDK、平台或启动参数后需重跑实验。