第一章:Go GC的基本原理与演进历程
Go 的垃圾回收器(GC)是其运行时系统的核心组件,采用三色标记-清除(Tri-color Mark-and-Sweep)算法,兼顾低延迟与高吞吐。自 Go 1.0 起,GC 经历了从 Stop-The-World(STW)到并发、增量、低延迟的持续演进:1.1 版本引入并发标记,1.5 实现完全并发的标记-清除(显著降低 STW 时间),1.8 引入混合写屏障(Hybrid Write Barrier)消除“灰色对象漏标”风险,1.12 后稳定在约 1ms 的 P99 STW 目标,2.0 路线图进一步聚焦于软实时特性。
核心机制:三色抽象与写屏障
GC 将对象分为白(未访问)、灰(已访问但子对象未扫描)、黑(已访问且子对象全扫描)三类。并发执行时,为防止程序修改对象引用导致漏标,Go 在 1.8+ 默认启用混合写屏障:当指针字段被赋值时,自动将目标对象标记为灰色,并记录到灰色队列。该机制无需 STW 即可保证标记完整性。
关键演进节点对比
| 版本 | STW 典型时长 | 并发性 | 写屏障类型 | 主要改进 |
|---|---|---|---|---|
| Go 1.4 | ~100–300ms | 无 | 无 | 纯 STW 标记 |
| Go 1.5 | ~10–50ms | 并发标记 | Dijkstra | 首次支持并发标记 |
| Go 1.8 | 完全并发 | 混合写屏障 | 消除漏标,稳定低延迟 |
查看当前 GC 行为的调试方法
可通过 GODEBUG=gctrace=1 启用 GC 追踪日志:
GODEBUG=gctrace=1 ./myapp
输出示例:
gc 1 @0.012s 0%: 0.012+1.2+0.016 ms clock, 0.048+0.36/0.72/0.16+0.064 ms cpu, 4->4->2 MB, 5 MB goal, 4 P
其中 0.012+1.2+0.016 表示 STW 扫描(0.012ms)、并发标记(1.2ms)、STW 清扫(0.016ms);4->4->2 表示标记前堆大小、标记后堆大小、清扫后堆大小(单位 MB)。
触发强制 GC 的方式
开发调试中可显式触发一次 GC 循环:
import "runtime"
// ...
runtime.GC() // 阻塞直到 GC 完成,返回后堆内存已回收
该调用不改变 GC 策略,仅强制启动一轮标记-清除周期,适用于内存敏感场景下的确定性回收控制。
第二章:Go三色标记并发GC的底层机制
2.1 从STW到混合写屏障:GC停顿时间压缩的技术路径
早期垃圾回收器依赖全局Stop-The-World(STW),所有应用线程暂停直至标记与清理完成,停顿时间随堆大小线性增长。
混合写屏障的协同机制
现代GC(如Go 1.21+、ZGC)采用混合写屏障(Hybrid Write Barrier),在赋值操作中同时记录旧对象引用失效与新对象跨代引用:
// Go runtime 中简化版混合写屏障伪代码
func writeBarrier(ptr *uintptr, newobj unsafe.Pointer) {
// 1. 将原指针指向的对象标记为"灰色"(需重新扫描)
shade(oldobj)
// 2. 将新对象加入并发标记队列(仅当 newobj 在年轻代且 ptr 在老年代)
if isOldGen(ptr) && isYoungGen(newobj) {
enqueueConcurrentMark(newobj)
}
}
shade()确保漏标被修复;enqueueConcurrentMark()触发增量标记,避免STW扫描整个老年代。
关键演进对比
| 阶段 | STW时长 | 并发能力 | 写屏障类型 |
|---|---|---|---|
| 经典标记清除 | O(堆大小) | 无 | 无 |
| CMS | 降低但残留 | 部分 | 卡片标记 |
| ZGC/Shenandoah | 全并发 | 混合(读+写) |
graph TD
A[应用线程写入 obj.field = newobj] --> B{混合写屏障触发}
B --> C[标记 oldobj 为灰色]
B --> D[若跨代,将 newobj 推入并发标记队列]
C & D --> E[GC线程异步处理标记队列]
2.2 标记阶段的并发协作模型与灰色对象队列实践
在并发标记过程中,GC线程与用户线程并行执行,需保证对象图遍历的一致性。核心挑战在于:用户线程可能修改引用关系,导致漏标(如将白色对象赋值给黑色对象)。
灰色对象队列设计
采用 增量更新(SATB)+ 灰色队列缓冲 混合策略:
- 所有新入队的可达对象标记为灰色,加入无锁
ConcurrentLinkedQueue - GC工作线程从队列中
poll()取出对象,扫描其字段,将新发现的白色对象标记为灰色并入队
// 灰色队列原子入队示例(简化)
public void pushGray(Object obj) {
if (obj != null && obj.color == WHITE) {
obj.color = GRAY; // 原子写入颜色
grayQueue.offer(obj); // 无锁入队
}
}
offer()保证线程安全;color字段需 volatile 或通过内存屏障确保可见性;WHITE→GRAY转换是标记起点,避免重复入队。
并发协作关键机制
- 使用 三色标记抽象:白(未访问)、灰(已入队待扫描)、黑(已扫描完成)
- 用户线程写屏障拦截引用更新,触发 SATB 预写快照或增量更新记录
| 协作维度 | 机制 | 保障目标 |
|---|---|---|
| 内存可见性 | volatile color + CAS | 防止灰/白状态错乱 |
| 队列竞争 | 无锁队列 + 批量窃取 | 降低线程间争用 |
| 漏标防护 | 写屏障 + 增量更新日志 | 维持“黑→白”路径不丢失 |
graph TD
A[用户线程修改引用] --> B{写屏障触发}
B --> C[记录旧引用至SATB缓冲区]
B --> D[若新引用为白色→立即置灰入队]
C --> E[标记结束前重扫SATB日志]
2.3 清扫阶段的惰性回收与内存归还策略分析
在并发标记-清除(CMS)及ZGC等现代垃圾收集器中,清扫阶段不再同步归还物理内存,而是采用惰性策略延迟释放。
惰性归还的触发条件
- 堆内存空闲率持续高于阈值(如70%)达3个GC周期
- 系统空闲时间窗口超过500ms(由
os::elapsed_counter()判定) - 显式调用
System.gc()(仅作提示,不强制)
内存归还粒度控制
// JVM参数示例:控制归还最小单位(页大小对齐)
-XX:MinHeapFreeRatio=40 // 触发归还的最低空闲比例
-XX:MaxHeapFreeRatio=70 // 归还后目标空闲比例上限
-XX:G1PeriodicGCInterval=10000 // ZGC每10s检查一次归还时机
该配置使JVM在保证吞吐前提下,避免频繁madvise(MADV_DONTNEED)系统调用开销;MinHeapFreeRatio过低会导致内存驻留过高,过高则引发抖动。
回收路径对比
| 策略 | 即时归还 | 惰性归还 | 混合策略 |
|---|---|---|---|
| 延迟 | 0ms | 100–500ms | 动态自适应 |
| 系统调用频次 | 高(每次GC) | 低(按需触发) | 中等 |
graph TD
A[清扫完成] --> B{空闲内存 > MaxHeapFreeRatio?}
B -->|是| C[加入待归还页链表]
B -->|否| D[暂存至FreeList]
C --> E[后台线程定时扫描]
E --> F[madvise系统调用归还]
2.4 GC触发阈值动态调节与GOGC参数的工程化调优
Go 运行时通过 GOGC 控制堆增长倍数触发 GC,但静态配置易导致抖动或内存泄漏。现代服务需结合实时指标动态调节。
GOGC 的底层行为
当 GOGC=100 时,GC 在堆分配量达上一轮回收后存活对象大小 × 2 时触发。其本质是:
// runtime/mgc.go 中关键逻辑片段(简化)
if heapLive >= heapGoal {
gcStart(gcTrigger{kind: gcTriggerHeap})
}
// heapGoal = heapMarked + heapMarked * uint64(GOGC) / 100
heapMarked 是上轮标记存活对象量;GOGC 直接线性放大目标阈值,非百分比增量。
动态调节策略对比
| 策略 | 响应延迟 | 内存波动 | 实现复杂度 |
|---|---|---|---|
| 固定 GOGC | 高 | 大 | 低 |
| 基于 P95 分配速率 | 中 | 中 | 中 |
| Prometheus + PID 控制 | 低 | 小 | 高 |
自适应调节流程
graph TD
A[采集 heap_live, alloc_rate, GC pause] --> B{是否超阈值?}
B -->|是| C[计算新 GOGC = f(heap_live, pause_ms)]
B -->|否| D[维持当前值]
C --> E[调用 debug.SetGCPercent(newGOGC)]
核心原则:在 P99 GC 暂停
2.5 Go 1.22+ Arena内存管理对GC压力的结构性缓解
Go 1.22 引入的 arena 包(runtime/arena)允许开发者显式声明一组对象共享生命周期,从而绕过全局 GC 追踪。
Arena 的核心契约
- 所有 arena 分配的对象不可逃逸到 arena 生命周期外
- arena 本身需显式
Free(),释放即整体回收,零 GC 扫描开销
arena := new(runtime.Arena)
p := arena.Alloc(1024, layout) // layout 定义类型对齐与大小
// p 指向的内存永不被 GC 标记或清扫
Alloc 不触发写屏障,layout 决定内存对齐(如 unsafe.AlignOf(int64{})),避免碎片;arena 生命周期由用户控制,彻底移除该内存块的 GC 元数据注册。
典型适用场景
- 短时批处理(如 HTTP 中间件上下文)
- 零拷贝序列化缓冲区
- 游戏帧级对象池
| 特性 | 传统堆分配 | Arena 分配 |
|---|---|---|
| GC 可达性追踪 | ✅ | ❌ |
| 内存释放粒度 | 对象级 | arena 整体 |
| 分配延迟 | ~10ns | ~2ns |
graph TD
A[申请 Arena] --> B[批量 Alloc 对象]
B --> C[业务逻辑使用]
C --> D[显式 Free Arena]
D --> E[所有对象原子释放]
第三章:Disable GC模式下的内存生命周期重构
3.1 runtime.GC()手动触发的时机决策与性能观测闭环
手动调用 runtime.GC() 并非替代自动 GC 的常规手段,而是用于可控场景下的确定性回收时机干预,例如长周期批处理结束、内存敏感型服务冷启动后预热完成、或大型资源池重置前。
触发前的关键判断条件
- 当前堆内存使用量 > 阈值(如
runtime.ReadMemStats().HeapAlloc > 80% * GOGC * HeapInuse) - 上次 GC 距今已超
time.Minute且无活跃 goroutine 峰值 - 外部信号(如 HTTP
/debug/gc端点显式请求)
典型安全调用模式
func safeManualGC() {
// 避免并发触发与阻塞主线程
select {
case <-time.After(100 * time.Millisecond): // 退避窗口
runtime.GC() // 同步阻塞,等待标记-清除完成
default:
return
}
}
此代码确保 GC 不在高负载窗口内执行;
runtime.GC()是同步调用,会阻塞当前 goroutine 直至全局 STW 结束并完成清扫。需配合debug.SetGCPercent(-1)临时禁用自动 GC,避免竞态。
性能观测闭环要素
| 维度 | 工具/指标 | 采集方式 |
|---|---|---|
| GC 暂停时间 | GCPauseNs(/debug/pprof/trace) |
pprof trace 抽样 |
| 堆增长速率 | MemStats.HeapAlloc, HeapSys |
runtime.ReadMemStats |
| 触发频次 | 自定义计数器 + Prometheus Exporter | HTTP metrics endpoint |
graph TD
A[业务事件触发] --> B{满足阈值?}
B -->|是| C[执行 runtime.GC()]
B -->|否| D[记录 skip log]
C --> E[ReadMemStats → 更新监控]
E --> F[对比上次 GC Pause Δt]
F --> G[动态调整下次触发策略]
3.2 对象逃逸分析失效场景下栈/堆分配的显式控制
当 JIT 编译器因动态调用、反射或跨线程共享等行为无法判定对象作用域时,逃逸分析自动失效,JVM 默认将对象分配至堆。
显式栈分配的实践路径
Java 16+ 的 ScopedValue 和 Loom 的虚拟线程上下文提供轻量级栈绑定能力:
ScopedValue.where(KEY, "request-id-123", () -> {
// KEY 绑定值在当前栈帧生命周期内有效,不逃逸
processRequest(); // 避免创建堆上 Context 对象
});
逻辑:
ScopedValue.where利用栈帧快照捕获绑定,绕过逃逸判定;KEY为ScopedValue<T>实例,T类型需为不可变或线程封闭。
常见失效场景对比
| 场景 | 是否触发逃逸 | 栈分配可行性 |
|---|---|---|
| 方法返回对象引用 | 是 | 否(需堆) |
| 存入静态 ConcurrentHashMap | 是 | 否 |
| 仅在局部 lambda 中使用 | 否(通常) | 是(配合 ScopedValue) |
graph TD
A[对象创建] --> B{逃逸分析}
B -->|失败| C[强制堆分配]
B -->|成功| D[标量替换/栈分配]
C --> E[显式控制:ScopedValue / try-with-resources]
3.3 长生命周期对象隔离与GC禁区(No-GC Zone)构建实践
在高吞吐实时系统中,将缓存元数据、连接池句柄、序列化Schema等长生命周期对象与短生命周期业务对象物理隔离,可显著降低GC压力。
核心隔离策略
- 使用独立的
ByteBuffer池配合DirectMemory托管非堆内存 - 通过
ClassLoader隔离加载核心元数据类,避免被频繁GC扫描 - 利用
Unsafe分配固定地址对象(需JVM参数-XX:+UnlockUnstableVMOptions -XX:MaxDirectMemorySize=4g)
No-GC Zone 内存布局示例
// 构建只读元数据区:生命周期与JVM同寿
public class NoGCZone {
private static final ByteBuffer METADATA_BUFFER =
ByteBuffer.allocateDirect(1024 * 1024); // 1MB direct buffer
static {
METADATA_BUFFER.asCharBuffer().put("SCHEMA_V3"); // 静态初始化即固化
METADATA_BUFFER.flip();
}
}
逻辑分析:
allocateDirect绕过堆内存,不受Young/Old GC管理;static final引用确保元数据区永不被回收。flip()后buffer进入只读就绪态,避免运行时修改触发内存屏障。
| 区域类型 | GC可见性 | 典型用途 | 生命周期 |
|---|---|---|---|
| Heap(普通对象) | ✅ | 临时DTO、请求上下文 | 秒级 |
| Direct Memory | ❌ | 序列化缓冲、元数据快照 | 进程级 |
| Metaspace | ⚠️(仅类卸载) | ClassLoader隔离元数据 | 类加载器存活期 |
graph TD
A[应用线程] -->|只读访问| B[No-GC Zone]
B --> C[Direct Buffer Pool]
B --> D[Metaspace Schema]
C --> E[零拷贝序列化]
D --> F[反射跳过Class.forName]
第四章:Arena内存池在超低延迟系统中的落地范式
4.1 Arena初始化、预分配与跨goroutine安全复用设计
Arena 是 Go 内存池的核心抽象,其设计需兼顾低延迟、零GC压力与并发安全性。
初始化策略
构造时指定 sizeHint(如 4KB),触发底层 mmap 预留虚拟内存页,但不立即提交物理页:
func NewArena(sizeHint int) *Arena {
mem, _ := mmap(nil, sizeHint, prot, flags, -1, 0)
return &Arena{
base: mem,
limit: uintptr(mem) + uintptr(sizeHint),
offset: atomic.Uintptr{},
}
}
offset 使用原子类型避免锁竞争;mmap 的 MAP_ANONYMOUS 标志确保无文件后端,prot=PROT_READ|PROT_WRITE 支持读写。
跨goroutine复用保障
- 所有分配/重置操作基于 CAS 原子偏移更新
- 禁止直接暴露裸指针,仅通过
Alloc(n)返回安全切片
| 特性 | 实现方式 |
|---|---|
| 零拷贝复用 | 复位 offset 即可重用内存区域 |
| 线程安全 | 无锁 CAS + 内存屏障 |
| 边界防护 | 每次 Alloc 校验 offset + n < limit |
graph TD
A[goroutine A Alloc] -->|CAS offset| B[共享 base+offset]
C[goroutine B Alloc] -->|CAS offset| B
B --> D[返回 []byte 指向同一 arena]
4.2 基于Arena的结构体对象池与零拷贝序列化协同优化
Arena内存池为结构体分配提供连续、可批量回收的内存块,避免频繁堆分配开销;零拷贝序列化(如Cap’n Proto或FlatBuffers)则直接复用Arena中已布局的二进制视图,跳过序列化/反序列化中间拷贝。
协同设计核心机制
- Arena预分配固定大小页(如64KB),按结构体对齐(
alignof(Header))切分slot - 对象池复用Arena页内空闲slot,生命周期由Arena释放统一管理
- 序列化器直接返回
const uint8_t*指针+长度,不触发memcpy
关键代码片段
struct alignas(16) Packet {
uint32_t seq;
char data[1024];
};
Arena arena(64_KB);
ObjectPool<Packet> pool(arena);
auto* pkt = pool.acquire(); // 零分配开销,仅指针偏移
pkt->seq = 123;
// FlatBufferBuilder复用arena.buffer()起始地址 → 真正零拷贝
arena.buffer()返回内部uint8_t*基址,ObjectPool::acquire()通过原子游标偏移获取slot,避免锁竞争;alignas(16)确保SIMD友好的内存对齐,提升序列化吞吐。
| 优化维度 | 传统方式 | Arena+零拷贝 |
|---|---|---|
| 分配延迟 | ~50ns(malloc) | ~2ns(指针加法) |
| 序列化拷贝次数 | 2次(obj→buf→net) | 0次(共享内存视图) |
graph TD
A[请求Packet实例] --> B{对象池有空闲slot?}
B -->|是| C[返回Arena内预对齐指针]
B -->|否| D[向Arena申请新页]
C --> E[FlatBufferBuilder::Finish() 直接映射该地址]
D --> C
4.3 Arena内存泄漏检测与runtime/debug.ReadGCStats集成监控
Arena内存泄漏的典型特征
Arena分配器若未显式释放,会导致对象长期驻留堆中,绕过GC回收路径。常见于高频创建/销毁短生命周期对象的场景(如网络连接池、序列化缓冲区)。
GC统计与Arena泄漏的关联分析
runtime/debug.ReadGCStats 提供 NumGC、PauseTotalNs 等指标,但无法直接追踪Arena。需结合 runtime.ReadMemStats 中 HeapAlloc 与 HeapSys 的持续增长趋势交叉判断。
集成监控代码示例
var lastGCStats debug.GCStats
func checkArenaLeak() {
var stats debug.GCStats
debug.ReadGCStats(&stats)
if stats.NumGC > lastGCStats.NumGC &&
stats.PauseTotalNs-lastGCStats.PauseTotalNs > 1e9 { // 暂停超1s
log.Printf("suspected arena leak: GC pressure ↑, HeapAlloc=%.2fMB",
float64(memstats.HeapAlloc)/1024/1024)
}
lastGCStats = stats
}
该函数每秒调用一次,通过GC频次与暂停时间突增识别内存压力异常;PauseTotalNs 差值反映GC负担变化,是Arena泄漏引发GC风暴的关键信号。
监控指标对比表
| 指标 | 正常波动范围 | 泄漏敏感度 |
|---|---|---|
HeapAlloc |
±10% 峰值 | ★★★★☆ |
NumGC |
稳定周期性增长 | ★★★☆☆ |
PauseTotalNs |
单次 | ★★★★★ |
4.4 混合内存管理模式:Arena + Pool + 原生堆的分层治理方案
混合内存管理模式通过三级分层实现性能与灵活性的平衡:短生命周期对象交由 Arena 快速分配/批量回收;中频复用对象(如网络缓冲区)托管于对象池(Pool);长周期或大块数据(如模型权重)则保留在原生堆,由 GC 精细管理。
分层职责划分
- Arena 层:零碎片、无释放开销,适用于 request-scoped 对象
- Pool 层:预分配 + 引用计数,降低 GC 压力
- 原生堆层:支持复杂引用图与跨生命周期对象
内存分配策略示例
// Arena 分配(线程局部,无析构)
let arena = Arena::new();
let buf = arena.alloc_slice::<u8>(1024); // 仅标记偏移,O(1)
// Pool 复用(带 reset 语义)
let pool = BufferPool::default();
let mut buf = pool.acquire(); // 可能复用已归还实例
buf.reset(); // 清理状态,非释放内存
// 原生堆(交由 Rust Box 或 GC 管理)
let model = Box::new(DenseLayer::load_from_disk("mlp.bin"));
arena.alloc_slice() 仅更新内部指针,不触发系统调用;pool.acquire() 返回可重用对象,reset() 是逻辑清零而非内存释放;Box::new() 触发标准堆分配,受 Drop 和 GC 协同管理。
吞吐量对比(100K 分配/秒)
| 模式 | 平均延迟 (ns) | GC 暂停时间 (ms) |
|---|---|---|
| 纯 Arena | 8 | 0 |
| Arena+Pool | 42 | 1.2 |
| 全堆分配 | 310 | 24.7 |
graph TD
A[请求到达] --> B{对象生命周期}
B -->|≤10ms| C[Arena 分配]
B -->|10ms–5s| D[Pool 获取]
B -->|>5s 或 >1MB| E[原生堆分配]
C --> F[请求结束时整体释放]
D --> G[使用后归还至 Pool]
E --> H[由 GC 异步回收]
第五章:超低延迟Go服务的GC治理全景图
GC对P99延迟的隐性冲击
某高频交易网关在压测中出现P99延迟从120μs突增至3.8ms的异常波动。通过go tool trace分析发现,每次GC pause(STW)平均耗时2.1ms,且与业务请求峰值高度重合。关键路径上存在大量临时[]byte拼接操作,导致年轻代对象存活率高达65%,触发频繁minor GC。
内存分配模式重构实践
将JSON序列化从json.Marshal()改为预分配缓冲池+json.Encoder流式写入:
var encoderPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 0, 4096)
return &bytes.Buffer{Buf: buf}
},
}
// 使用时
buf := encoderPool.Get().(*bytes.Buffer)
buf.Reset()
enc := json.NewEncoder(buf)
enc.Encode(data) // 避免逃逸到堆
改造后每秒GC次数从87次降至3次,young generation GC时间减少92%。
运行时参数精细化调优
| 参数 | 原值 | 调优值 | 效果 |
|---|---|---|---|
GOGC |
100 | 25 | 减少GC触发频率,但需配合内存监控 |
GOMEMLIMIT |
unset | 1.2GiB | 防止RSS持续增长导致OOMKilled |
GODEBUG=gctrace=1 |
off | on | 生产环境启用采样日志(每100次GC输出一次) |
对象复用与零拷贝策略
订单匹配引擎中,将每笔委托单创建的Order结构体改为对象池管理:
var orderPool = sync.Pool{
New: func() interface{} {
return &Order{
Price: new(decimal.Decimal),
Qty: new(decimal.Decimal),
}
},
}
// 关键点:避免在Reset方法中保留引用
func (o *Order) Reset() {
*o.Price = decimal.Zero
*o.Qty = decimal.Zero
o.Symbol = ""
o.Side = 0
}
实测GC压力下降41%,对象分配延迟标准差从±8.3μs收敛至±1.2μs。
GC指标监控体系构建
部署Prometheus exporter采集以下核心指标:
go_gc_duration_seconds(分位数直方图)go_memstats_heap_alloc_bytes(实时追踪堆增长拐点)- 自定义指标
gc_pause_over_100us_total(通过trace解析STW超阈值事件)
告警规则配置:连续3分钟histogram_quantile(0.99, rate(go_gc_duration_seconds_bucket[5m])) > 0.0001触发P1告警。
生产环境灰度验证流程
在Kubernetes集群中采用渐进式发布:
- 先在5%流量Pod设置
GOGC=25并开启GODEBUG=mcsweep=1 - 持续采集15分钟
pprof/heap对比对象分布变化 - 通过eBPF工具bcc/biolatency验证内核调度延迟无劣化
- 全量切换前执行混沌工程:注入
SIGUSR1强制GC验证服务韧性
内存泄漏定位黄金路径
当go_memstats_heap_inuse_bytes持续单向增长时,按优先级执行:
go tool pprof -alloc_objects http://localhost:6060/debug/pprof/heap查找高频分配点go tool pprof -inuse_objects http://localhost:6060/debug/pprof/heap定位长生命周期对象- 结合
runtime.ReadMemStats定期dumpMallocs与Frees差值验证泄漏速率
GC与CPU亲和性协同优化
将GC worker线程绑定至专用CPU核(非业务线程使用核):
# 启动脚本中设置
taskset -c 4-7 ./trading-gateway \
GOMAXPROCS=8 \
GODEBUG=schedtrace=1000
配合Linux cgroups限制GC线程CPU配额,避免STW期间抢占业务线程调度资源。实测在32核机器上,GC期间业务线程调度延迟抖动降低76%。
