第一章:蔡超与万亿级消息系统的GC演进之路
在支撑日均超1.2万亿条消息投递的分布式消息平台中,JVM垃圾回收曾是系统稳定性的最大隐性瓶颈。早期采用CMS收集器时,单节点Full GC频次达每小时3–5次,STW时间峰值突破8秒,直接导致P99消息延迟飙升至分钟级。蔡超团队没有止步于参数调优,而是推动从运行时机制到底层内存模型的系统性重构。
关键技术决策路径
- 放弃CMS,全面迁移至G1收集器,并启用
-XX:+UseStringDeduplication消除重复消息体字符串内存冗余 - 将消息序列化层由JSON切换为Protobuf v3 + 自定义零拷贝缓冲区池,减少堆内临时对象生成
- 引入Off-Heap Message Cache:使用
sun.misc.Unsafe管理堆外内存存储消息元数据,配合Cleaner异步释放,使Young GC吞吐量提升47%
GC参数调优实录
核心JVM启动参数如下(适用于16核/64GB容器环境):
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=150 \
-XX:G1HeapRegionSize=1M \
-XX:G1NewSizePercent=30 \
-XX:G1MaxNewSizePercent=50 \
-XX:G1MixedGCCountTarget=8 \
-XX:+G1UseAdaptiveIHOP \
-XX:G1InitialHeapOccupancyPercent=45 \
-XX:+UseStringDeduplication \
-XX:+AlwaysPreTouch \
-Xlog:gc*:file=/var/log/jvm/gc.log:time,tags,level:filecount=5,filesize=100M
该配置经A/B测试验证:混合GC周期缩短32%,晋升失败(Evacuation Failure)归零,且日志中G1 Evacuation Pause平均耗时稳定在92±11ms。
监控与反馈闭环
| 建立三维度GC健康度看板: | 指标 | 健康阈值 | 采集方式 |
|---|---|---|---|
| Young GC频率 | ≤12次/分钟 | JVM MXBean CollectionCount |
|
| 平均GC暂停时间 | -Xlog:gc+stats解析 |
||
| 堆外内存泄漏速率 | jcmd <pid> VM.native_memory summary定期快照比对 |
所有告警自动触发jstack -l <pid>与jmap -histo:live <pid>快照采集,并推送至SRE值班通道。
第二章:Golang GC核心机制深度解析
2.1 三色标记算法的工程实现与暂停点剖析
三色标记在实际 GC 实现中需精细控制并发安全与停顿边界。核心在于将对象图遍历拆解为可抢占的原子单元,并在安全点(Safepoint)插入检查。
暂停点注入机制
JVM 在方法返回、循环回边等位置插入 safepoint poll 指令,触发线程状态同步:
// HotSpot VM 中的 safepoint 检查伪代码
if (Thread::current()->polling_page() == Page::safepoint_poll_value()) {
SafepointMechanism::block_if_safepoint();
}
polling_page 是每个线程独占的内存页,由 VM 线程在安全区域开始前统一置为特殊值;block_if_safepoint() 阻塞直至全局安全点达成。
标记阶段的三色状态映射
| 颜色 | 内存表示 | 含义 |
|---|---|---|
| 白 | 未访问位未置位 | 尚未扫描,可能为垃圾 |
| 灰 | 在标记栈中 | 已访问但子引用未处理 |
| 黑 | marked_bit = true | 已完全扫描,子对象全为黑 |
并发写屏障关键路径
// G1 的 SATB 写屏障片段(简化)
void write_barrier(oop* field, oop new_val) {
if (new_val != nullptr && !is_marked(new_val)) {
enqueue_to_satb_buffer(new_val); // 记录被覆盖的旧引用
}
}
该屏障捕获“被丢弃的白色对象”,防止漏标;enqueue_to_satb_buffer 原子写入线程本地缓冲区,避免锁竞争。
graph TD A[应用线程执行] –> B{是否到达 safepoint poll?} B –>|是| C[挂起并等待 VM 线程通知] B –>|否| D[继续执行 Java 字节码] C –> E[VM 线程完成全局标记/清理] E –> F[恢复所有应用线程]
2.2 堆内存分代假设失效场景及实测验证(Kafka Proxy压测数据)
在 Kafka Proxy 高吞吐压测中,当消息体平均达 128KB 且生产速率稳定在 8k msg/s 时,Young GC 频率未显著上升,但 Old Gen 却在 12 分钟内从 30% 涨至 95%,触发频繁 CMS 并发失败——典型分代假设(“多数对象朝生夕死”)失效。
观测关键指标
- 吞吐量:8,240 msg/s
- 对象平均生命周期:> 18s(远超 Young GC 间隔 1.2s)
- Eden 区存活率:68%(非典型
JVM 参数配置(实测有效)
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=1M \ # 匹配大消息体粒度
-XX:G1NewSizePercent=30 \ # 扩大年轻代基线
-XX:G1MixedGCCountTarget=8
该配置将混合回收触发阈值从默认 80% 提前至 65%,缓解 Old Gen 突增压力;G1HeapRegionSize=1M 避免大消息跨 Region 拆分导致的复制开销。
| GC 类型 | 频次(/min) | 平均暂停(ms) | 晋升量(MB/s) |
|---|---|---|---|
| Young GC | 4.2 | 86 | 1.3 |
| Mixed GC | 0.7 | 210 | 14.8 |
失效根因流程
graph TD
A[Producer 发送 128KB 消息] --> B[Proxy 内存池分配 DirectByteBuffer]
B --> C[对象直接进入 Old Gen:-XX:PretenureSizeThreshold=64K 已设]
C --> D[Old Gen 碎片化加剧]
D --> E[Concurrent Mode Failure 频发]
2.3 GC触发阈值GOGC的动态敏感性建模与反直觉案例
GOGC 并非静态放大器,而是与上一次GC后存活堆大小动态耦合的相对阈值:next_gc ≈ live_heap × (1 + GOGC/100)。
反直觉现象:GOGC=100 时,内存占用反而飙升
当应用存在周期性大对象缓存(如每分钟加载100MB热数据并长期持有),即使GOGC=100,下一次GC触发点将被“锚定”在100MB×2=200MB——但若缓存持续增长至190MB后突增80MB,则立即触发GC;而回收仅释放临时对象(
// 模拟GOGC动态计算逻辑(Go runtime/src/runtime/mgc.go简化)
func computeNextGC(liveHeap uint64, gogc int32) uint64 {
if gogc < 0 {
return ^uint64(0) // disable GC
}
return liveHeap + liveHeap*uint64(gogc)/100
}
liveHeap是上一轮GC结束时的实际存活堆字节数,非当前堆总量;gogc=100表示允许堆增长至存活量的200%,但若存活量本身因缓存膨胀而阶梯式上升,GC阈值将正反馈式抬升。
敏感性量化对比(GOGC=50 vs 100)
| GOGC设置 | 初始存活堆 | 触发下一次GC所需新增分配 |
|---|---|---|
| 50 | 100 MB | 50 MB |
| 100 | 100 MB | 100 MB |
注意:当存活堆从100MB→180MB后,GOGC=100的新阈值变为360MB,新增容忍量骤增至180MB——阈值弹性随存活量线性放大。
graph TD A[上轮GC结束] –> B[测量liveHeap] B –> C[计算nextGC = liveHeap × (1+GOGC/100)] C –> D[持续分配直至heap ≥ nextGC] D –> E[触发GC并重测newLiveHeap] E –> C
2.4 辅助GC(Assist GC)在高并发写入链路中的隐式开销定位
在高吞吐写入场景中,Go runtime 的辅助GC(Assist GC)会动态插入写屏障协助标记,但其触发阈值与 Goroutine 本地分配计数强耦合,易引发非预期的协程阻塞。
数据同步机制
当 Goroutine 分配内存速率超过 gcTriggerHeap 阈值时,runtime 强制进入 assist 模式:
// src/runtime/mgc.go 中 assistAlloc 的关键逻辑
if assistBytes > 0 {
gcAssistAlloc(assistBytes) // 阻塞式协助扫描
}
assistBytes 由 gcController.assistWorkPerByte 动态计算,单位为“等效标记字节数”,直接决定协程在写入热点路径上的等待时长。
常见诱因归类
- 写密集型结构体频繁构造(如日志 Entry、Protobuf 消息)
- sync.Pool 未复用导致对象生命周期过短
- Pacer 误判:
heapLive统计延迟导致 assist 突增
性能影响对比(典型 16 核实例)
| 场景 | 平均写入延迟 | Assist 占比 CPU 时间 |
|---|---|---|
| 无 assist 压力 | 12 μs | |
| 高频小对象分配 | 89 μs | 18.7% |
graph TD
A[写入请求] --> B{分配字节数 > assistTrigger?}
B -->|是| C[进入 gcAssistAlloc]
B -->|否| D[常规分配]
C --> E[暂停分配,执行标记工作]
E --> F[返回分配结果]
2.5 STW与Mark Assist时间分布的pprof火焰图交叉分析法
在GC调优中,STW(Stop-The-World)与Mark Assist(标记辅助)的时间耦合性常被低估。二者在pprof火焰图中呈现空间重叠但语义分离:STW集中于runtime.gcStart栈顶,而Mark Assist分散在gcAssistAlloc调用链中。
火焰图交叉定位方法
使用以下命令生成双视角采样:
# 同时捕获调度延迟与GC辅助标记
go tool pprof -http=:8080 \
-tags 'gc,alloc' \
./binary http://localhost:6060/debug/pprof/profile?seconds=30
该命令启用
gc和alloc标签过滤,使火焰图自动高亮runtime.gcDrain与runtime.gcAssistAlloc路径;seconds=30确保覆盖至少1次完整GC周期,避免采样偏差。
关键指标对照表
| 指标 | STW阶段典型值 | Mark Assist阶段典型值 |
|---|---|---|
| 单次耗时(ms) | 1.2–4.8 | 0.03–0.8 |
| 调用频次/秒 | ~1.7 | ~210 |
执行路径依赖关系
graph TD
A[mutator alloc] --> B{是否触发 assist?}
B -->|是| C[gcAssistAlloc]
B -->|否| D[继续分配]
C --> E[drain mark queue]
E --> F[可能延长 STW 边界]
第三章:生产环境GC异常模式诊断体系
3.1 频繁GC(>50次/秒)与内存泄漏的鉴别决策树(含heap profile diff脚本)
当JVM GC频率持续超过50次/秒,需快速区分是瞬时压力峰值还是渐进式内存泄漏。
关键诊断信号
- 每次Full GC后老年代占用率未回落(>85%)→ 强泄漏嫌疑
- Young GC耗时稳定但次数激增 → 可能对象创建风暴,非泄漏
- Metaspace持续增长且不回收 → 类加载器泄漏典型特征
heap profile diff 自动比对脚本
# heap-diff.sh:基于jcmd + jhsdb对比两次hprof快照中top 20对象增量
jcmd $PID VM.native_memory summary scale=MB
jcmd $PID VM.native_memory detail | grep -A5 "Java Heap"
jhsdb jmap --heap --pid $PID --binaryheap 2>/dev/null | tail -n +2 | head -20 | awk '{print $1,$2}' > heap-now.txt
# 后续用diff -y heap-before.txt heap-now.txt | grep '>'
该脚本捕获堆顶对象分布变化,$1为类名,$2为实例数;差值>300%且连续三次上升,触发泄漏告警。
决策流程
graph TD
A[GC >50次/秒] --> B{Young GC占比 >95%?}
B -->|是| C[检查对象分配速率:-XX:+PrintGCDetails]
B -->|否| D[采样老年代存活对象:jhsdb clhsdb]
C --> E[突增?→ 业务逻辑问题]
D --> F[类实例持续增长?→ 内存泄漏]
| 指标 | 健康阈值 | 泄漏征兆 |
|---|---|---|
jstat -gc $PID 1s 中 OU 增速 |
>2 MB/s 持续10s | |
jmap -histo 中 char[] 占比 |
>40% 且递增 |
3.2 GC CPU占比突增时的runtime/metrics实时追踪实践
当GC导致CPU使用率陡升,需绕过采样延迟,直连Go运行时指标。
数据同步机制
runtime/metrics 提供纳秒级精度、无锁读取的指标快照:
import "runtime/metrics"
func trackGCPause() {
// 获取所有匹配GC暂停的指标(单位:nanoseconds)
set := metrics.All()
var gcPause *metrics.Description
for _, m := range set {
if m.Name == "/gc/pause:seconds" {
gcPause = &m
break
}
}
// 每100ms采集一次,避免高频抖动
ticker := time.NewTicker(100 * time.Millisecond)
for range ticker.C {
var ms []metrics.Sample
ms = append(ms, metrics.Sample{Name: gcPause.Name})
metrics.Read(ms) // 零分配、无GC的原子读取
pauseNs := int64(ms[0].Value.(float64) * 1e9)
if pauseNs > 5_000_000 { // >5ms,触发告警
log.Printf("GC pause spike: %d ns", pauseNs)
}
}
}
metrics.Read() 是轻量级快照,不阻塞GC;/gc/pause:seconds 是累积分布,需结合/gc/num:gc判断频率。
关键指标对照表
| 指标路径 | 含义 | 更新时机 |
|---|---|---|
/gc/pause:seconds |
最近GC暂停时长(分布) | 每次STW结束 |
/gc/num:gc |
累计GC次数 | 原子递增 |
/sched/goroutines:goroutines |
当前goroutine数 | Read()即时快照 |
实时响应流程
graph TD
A[Timer Tick] --> B[metrics.Read]
B --> C{pause > 5ms?}
C -->|Yes| D[Log + Push to Metrics Backend]
C -->|No| E[Continue]
3.3 “假稳定”堆增长下的scan object数量异常检测(基于go:linkname黑科技)
在GC标记阶段,runtime.gcMarkRootPrepare 与 runtime.gcDrain 间存在隐蔽的 scan object 数量漂移——堆看似稳定(GOGC=100 下 RSS 波动 mheap_.spanAlloc 中待扫描对象数却持续缓增。
核心观测点
runtime·gcControllerState.heapLive是估算值,不反映实时 mark queue 深度- 真实扫描压力藏于
work.markrootJobs与work.nproc的非线性耦合
黑科技接入方式
//go:linkname readMarkQueueLen runtime.gcMarkQueueLength
func readMarkQueueLen() uint64
//go:linkname readHeapLive runtime.heapLive
func readHeapLive() uint64
readMarkQueueLen直接读取work.markrootJobs长度,绕过 GC state 封装;参数无输入,返回当前根扫描任务队列长度(单位:job),精度达原子级。
异常判定逻辑
| 条件 | 阈值 | 含义 |
|---|---|---|
readMarkQueueLen() > 5e4 |
绝对阈值 | 标记根任务积压严重 |
readMarkQueueLen() / readHeapLive() > 0.003 |
相对比率 | 扫描负载畸高 |
graph TD
A[采集 markrootJobs 长度] --> B{>5e4?}
B -->|是| C[触发告警]
B -->|否| D{比率 >0.003?}
D -->|是| C
D -->|否| E[正常]
第四章:11项关键参数的协同调优策略
4.1 GOGC=10 vs GOGC=50在消息批量落盘场景的吞吐-延迟帕累托前沿对比
在高吞吐消息系统中,GC策略显著影响落盘延迟分布与吞吐稳定性。GOGC=10触发更激进回收,减少堆驻留对象,但增加STW频次;GOGC=50延缓GC周期,提升吞吐,却易引发单次长暂停。
实验配置关键参数
- 批量大小:256KB/批次
- 写入速率:12k msg/s(每msg≈128B)
- 堆初始大小:2GB(
GOMEMLIMIT未设)
吞吐-延迟帕累托前沿对比(10s窗口均值)
| GOGC | 吞吐(MB/s) | P99延迟(ms) | GC频次(/min) |
|---|---|---|---|
| 10 | 182 | 4.2 | 217 |
| 50 | 214 | 18.7 | 32 |
// 模拟批量落盘核心循环(含GC感知)
func writeBatch(batch [][]byte) {
runtime.GC() // 显式触发仅用于观测——实际禁用
for _, msg := range batch {
_, _ = diskFile.Write(msg) // 非阻塞IO需配合direct I/O绕过page cache
}
}
该代码强制暴露GC时机对落盘原子性的影响:GOGC=10下runtime.GC()调用更频繁,导致写入被STW打断,P99延迟压缩但吞吐承压;GOGC=50则使大batch更连贯,但内存压力峰值推高单次GC暂停。
内存行为差异示意
graph TD
A[GOGC=10] --> B[小堆增长步进]
A --> C[高频轻量GC]
D[GOGC=50] --> E[大堆缓存消息]
D --> F[低频但重GC]
4.2 GOMEMLIMIT与cgroup v2 memory.high协同控制的硬限兜底方案
Go 1.19+ 支持 GOMEMLIMIT 环境变量,将运行时内存上限与 cgroup v2 的 memory.high 协同使用,形成软限(high)+硬限(GOMEMLIMIT)双层防护。
协同机制原理
memory.high触发内核主动回收,降低 OOM 风险;GOMEMLIMIT驱动 Go runtime 提前触发 GC,避免分配超限;- 二者叠加可防止 runtime 在 cgroup 边界外“盲目冲刺”。
配置示例
# 设置 cgroup v2 软限(触发压力回收)
echo 512M > /sys/fs/cgroup/myapp/memory.high
# 设置 Go 运行时硬限(GC 强制阈值)
GOMEMLIMIT=480MiB ./myapp
逻辑分析:
GOMEMLIMIT应比memory.high低约 5–10%,为内核页缓存、runtime 元数据等预留空间;单位需一致(推荐MiB/M),避免解析歧义。
关键参数对照表
| 参数 | 类型 | 作用域 | 建议值 |
|---|---|---|---|
memory.high |
cgroup v2 | 内核级软限 | 512M(触发轻量回收) |
GOMEMLIMIT |
Go runtime | 用户态硬限 | 480MiB(强制 GC 触发点) |
graph TD
A[应用分配内存] --> B{Go runtime 检查 GOMEMLIMIT}
B -->|未超限| C[正常分配]
B -->|接近限值| D[提前触发 GC]
C --> E{内核检查 memory.high}
E -->|超 soft limit| F[reclaim inactive pages]
E -->|持续超限| G[OOM Killer 干预]
4.3 GODEBUG=gctrace=1+GODEBUG=madvdontneed=1组合对Page Cache回收的实测影响
Go 运行时默认在 sysFree 阶段对归还内存调用 MADV_DONTNEED,但 Linux 内核对其语义解释为“立即清空对应页并释放到 buddy 系统”,这会干扰 Page Cache 的生命周期管理。
实验观测关键现象
- 启用
GODEBUG=madvdontneed=1后,/proc/PID/status中Cached值下降明显; GODEBUG=gctrace=1显示 GC 后sys: X MB回收量激增,但Cached未同步回升 → 表明 Page Cache 被强制驱逐。
核心机制冲突
# 启动时注入调试标志
GODEBUG=gctrace=1,madvdontneed=1 ./myserver
此组合使 runtime 在
mmap区域释放时主动触发madvise(MADV_DONTNEED),绕过 VFS 层缓存保留逻辑,导致文件读写密集型服务 Page Cache 命中率骤降。
| 场景 | Page Cache 剩余率 | 平均延迟增长 |
|---|---|---|
| 默认配置 | 82% | — |
gctrace=1,madvdontneed=1 |
37% | +41% |
内存路径变更示意
graph TD
A[GC 触发内存归还] --> B{madvdontneed=1?}
B -->|是| C[调用 madvise DONTNEED]
B -->|否| D[仅 munmap]
C --> E[内核清空 Page Cache 条目]
D --> F[保留 Page Cache 可能性]
4.4 GC百分位延迟P999优化中GOGC与GOMAXPROCS的耦合调参矩阵
Go运行时中,P999 GC延迟对高敏感服务至关重要,而GOGC(堆增长触发比)与GOMAXPROCS(并行P数)存在非线性耦合:增加GOMAXPROCS可加速标记阶段,但若GOGC过高,会导致单次GC扫描堆更大,抵消并发收益。
调参冲突现象
GOMAXPROCS=8+GOGC=100→ P999=12ms(频繁小GC)GOMAXPROCS=32+GOGC=200→ P999=47ms(大停顿爆发)
关键实验矩阵(单位:ms)
| GOGC \ GOMAXPROCS | 4 | 16 | 32 |
|---|---|---|---|
| 50 | 8.2 | 7.9 | 11.3 |
| 150 | 9.6 | 6.1 | 8.7 |
| 250 | 14.4 | 10.2 | 15.8 |
// 启动时动态绑定:避免硬编码,支持热感知
func init() {
runtime.GOMAXPROCS(16) // 基于CPU quota自动缩放
debug.SetGCPercent(150) // 与GOMAXPROCS=16实测P999最优匹配
}
该配置在16核容器中将P999 GC延迟压至6.1ms——GOMAXPROCS提升标记并行度,GOGC=150则平衡堆增长与GC频次,避免STW膨胀。
耦合机制示意
graph TD
A[GOMAXPROCS↑] --> B[标记并发度↑]
C[GOGC↑] --> D[堆增长更快]
B & D --> E[单次GC工作量Δ]
E --> F{P999是否下降?}
F -->|是| G[存在最优交点]
F -->|否| H[需反向调节]
第五章:面向未来的GC演进思考
从ZGC在金融核心系统的毫秒级停顿实践
某头部券商于2023年将交易订单服务(QPS峰值12万,P99延迟要求-XX:+UseStringDeduplication(ZGC下该选项引发额外元空间扫描开销),并将对象分配速率从1.8GB/s压降至1.3GB/s以匹配ZGC的并发标记吞吐边界。
Shenandoah在Kubernetes边缘AI推理容器中的内存弹性验证
某自动驾驶公司部署的车载视觉模型推理服务(ARM64 + OpenJDK 21)采用Shenandoah GC。在内存受限的Edge Node(4GB RAM)上,通过-XX:ShenandoahUncommitDelay=1000与-XX:ShenandoahGuaranteedGCInterval=30000组合策略,实现堆内存自动收缩:空闲时段堆占用从3.2GB降至1.1GB,容器OOMKilled率下降92%。监控数据显示,Shenandoah的Brooks Pointer写屏障在Jetson Orin平台引入平均0.7%的CPU开销,但避免了传统GC因内存不足触发的频繁cgroup OOM Killer介入。
GC与硬件协同的前沿探索:CXL内存池化下的分代假设重构
| 技术维度 | 传统JVM假设 | CXL-Enabled JVM新约束 |
|---|---|---|
| 内存访问延迟 | DRAM: ~100ns | CXL Type-2设备: ~350ns(需重估晋升阈值) |
| 内存带宽成本 | 均一化带宽模型 | 近端DRAM vs 远端CXL内存带宽差达3.8× |
| 对象生命周期 | 年轻代存活率 | 模型权重参数在CXL池中长期驻留(>2h) |
实验表明:当将-XX:MaxNewSize设为总堆30%时,CXL场景下YGC耗时反而上升27%,而调整为15%后YGC平均耗时降低至1.2ms——证明经典分代假设在异构内存架构下必须被数据驱动重构。
// JDK 22+ 实验性API:显式内存域绑定(需-XX:+UnlockExperimentalVMOptions)
MemorySegment modelWeights = MemorySegment.mapFile(
Path.of("/dev/cxl/weight_pool"),
0, 2L * 1024 * 1024 * 1024, // 2GB CXL pool
FileChannel.MapMode.READ_WRITE,
SegmentScope.shared()
);
// JVM可据此优化GC对跨域引用的扫描策略
Project Loom协程与GC的耦合挑战
在基于虚拟线程的实时风控服务中(单JVM承载200万vthread),G1GC的Remembered Set更新成为瓶颈:每个vthread栈帧变更均触发RSet写入,导致Update RS阶段CPU占用率达63%。解决方案采用-XX:+G1UseAdaptiveIHOP动态调优,并将-XX:G1ConcRSLogBufferSize从默认1024提升至8192,使RSet日志缓冲区溢出率从17%降至0.3%,YGC暂停时间方差收敛至±0.05ms。
flowchart LR
A[Virtual Thread创建] --> B{是否访问堆外CXL内存?}
B -->|是| C[标记为“远端引用”]
B -->|否| D[走常规RSet更新路径]
C --> E[GC并发标记阶段跳过CXL区域扫描]
E --> F[仅在Mixed GC时按需加载CXL页]
RISC-V架构下GC寄存器屏障的指令级优化
阿里云龙芯3A5000(RISC-V 64)移植ZGC时发现:原x86_64的mov %rax, (%rdx)写屏障在RISC-V需拆分为sd a0, 0(a1)+fence w,w双指令,导致屏障开销增加40%。通过内联汇编重写ZBarrier::load_barrier_on_oop_field_preloaded,将fence指令合并至后续内存操作,最终使ZGC在RISC-V平台的STW时间从0.23ms降至0.14ms。
