• 周日. 9 月 13th, 2026

当垃圾回收看懂硬件:编程语言的内存革命

面向现代非均匀内存架构的分代式垃圾回收器示意图,缓存层级之间对象按访问热度迁移

在过去二十年里,垃圾回收器一直被当作一种”语言层面的便利”来讨论:只要写起来舒服,停顿大一点也能接受;只要吞吐量不掉,内存占用多一点也无所谓。但当主流工作负载从单核迁移到数十核、从一致内存迁移到非均匀内存架构、从本地 SSD 迁移到 NVMe 与持久内存之后,这种”语言层面”的视角开始失效——GC 必须看懂硬件。

一、旧假设为什么开始失效

传统分代 GC 的两条核心假设是:绝大多数对象”朝生暮死”,以及把对象从一个区域整体复制到另一个区域比原地修复便宜。这两条假设在 DRAM 一致内存时代几乎总是成立,所以很长一段时间里,工程界并不需要重新审视 GC 与硬件的关系。

变化发生在三个方向:

第一,DRAM 带宽与缓存未命中率之间的差距被进一步放大,一次”看似便宜”的指针追逐,其实跨越了三层缓存和一次内存控制器调度。

第二,NUMA 架构下,远端节点访问延迟比同节点高出数倍,GC 线程与 mutator 线程在不同节点之间迁移时,停顿的来源不再是算法本身,而是节点之间的跨链带宽。

第三,持久内存与高速 NVMe 改变了”内存”这个概念的边界,把对象放到哪一层存储介质上,开始影响 GC 的代价结构。

这些变化让 GC 的设计从”算法选择题”变成了”系统设计题”。

二、缓存感知:把对象放进对的位置

新一代 GC 在对象分配阶段就开始考虑缓存层级:小对象被放进与当前执行线程绑定的线程局部缓存区,中对象被放进与节点绑定的区域,大对象则被直接放进主堆之外的连续内存。这意味着对象分配的开销不再只是”找到一块空地”,而是”找到一块对当前线程、对当前 NUMA 节点、对当前工作负载最合适的位置”。

这种分配策略带来的副作用是显著的:跨核同步开销降低,缓存命中率提升,更重要的是 GC 自己触发的”伪共享”问题被显著缓解。

三、NUMA 亲和:让线程贴着它的内存跑

当 GC 决定在哪个节点上分配对象、在哪个节点上执行回收线程时,它实际上在回答一个跨学科问题:这一轮回收停顿的尾部延迟,由哪几个硬件参数决定?

新一代运行时把 mutator 线程与 GC 线程绑定到对象所在的节点,并允许在回收阶段把部分工作”借用”到其他节点——但只在跨节点带宽足够时这么做。结果是,停顿的尾部延迟有了可预测的模型,而不再像过去那样依赖”运气”。

四、压缩策略与硬件协作

对象晋升是分代 GC 中最贵的环节之一:把活对象从年轻代复制到老年代,并更新所有指向它的指针。新一代 GC 把这件事拆成两步——先在后台完成大部分对象移动,再在前台用极短的时间窗完成指针重映射与写屏障收敛。

这一步真正变化的,是与硬件预取、TLB 刷新、内存控制器调度节奏的协同:GC 会主动在预取窗口里发起对象移动,让硬件的预取器”以为”这是一次正常访问;会主动错开 TLB 刷新的高峰,让 mutator 不会突然遭遇命中率塌陷。最终效果是:停顿时长被”摊薄”到与硬件节奏对齐,而不是被一次性暴露。

五、编程语言生态正在重新洗牌

这些变化不是单点优化,而是正在重新定义”现代 GC 的硬指标”:p99 停顿、p99.9 停顿、跨 NUMA 节点的尾部延迟、以及在最坏情况下系统仍能维持的吞吐量下限。过去这些指标大多在监控面板上被作为”附录”展示,而现在,它们开始成为 GC 选型的首要参数。

对于工程团队来说,这意味着 GC 不再是可以”忽略”的部分:理解新一代 GC 的硬件模型,理解对象在缓存层级与 NUMA 节点之间的迁移规律,理解压缩、并发、停顿摊薄之间的取舍——这些知识,正在从”运行时内部细节”升级为”系统设计的必修课”。

六、给工程师的三个具体建议

第一,重新审视分配模式:短命小对象的过度分配,比过去更明显地影响 GC 工作量。

第二,把对延迟敏感的线程显式绑核,并与运行时的 NUMA 亲和策略对齐。

第三,建立 p99.9 停顿的监控,而不是只看平均停顿。这些调整几乎不需要改业务代码,却能让系统在峰值负载下保持更稳定的行为。

当垃圾回收器开始看懂硬件,”停顿”就不再是语言层面的副作用,而是系统设计的一部分。这条变化曲线才刚刚开始,但它已经足以改变我们对”性能”和”可预测性”的整体认识。

admin77

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注