如何实现Satori GC,既高吞吐又低延时且内存占用低?

摘要:前言 GC 的设计里一直有一个很难绕开的矛盾:高吞吐、低延时、低内存占用,通常很难同时做到。 传统做法里,想要更短的停顿,往往要把更多工作搬到并发阶段,甚至让平时的对象访问承担更高成本;想要更高的吞吐量,又往往意味着平时路径成本必须足够低,
前言 GC 的设计里一直有一个很难绕开的矛盾:高吞吐、低延时、低内存占用,通常很难同时做到。 传统做法里,想要更短的停顿,往往要把更多工作搬到并发阶段,甚至让平时的对象访问承担更高成本;想要更高的吞吐量,又往往意味着平时路径成本必须足够低,于是更多工作会堆到回收阶段;想要更低的内存占用,则又需要更积极地回收、整理和归还内存。 .NET 的 Satori GC 最有意思的地方不在于它把某个现有方向做得更激进,而在于它在设计上先问了自己:最频繁的那部分回收,真的必须是全局问题吗? GC 在做什么 对于托管语言来说,GC 要做的事情并不复杂: 找出哪些对象仍然存活 回收已经不可达的对象 必要时移动存活对象,减少碎片并维持分配效率 麻烦主要出在第三件事。对象一旦被移动,所有指向它们的引用都必须保持一致。为了保证这一点,GC 在某些阶段需要暂停用户线程。GC 暂停整个程序的行为通常也叫 STW。 分代 GC 之所以不需要每次都扫描整个堆,是因为它利用了一个非常重要的经验事实:大多数对象死得很快。 因此现代 GC 通常都会做分代: Gen 0:最新创建、也最容易很快死掉的对象 Gen 1:活过几轮回收,但还不算特别老的对象 Gen 2:已经证明自己很能活的长寿对象 分代的意义在于把最频繁的回收工作限制在最年轻的那一层。只要大多数短命对象都死在 Gen 0,那么大部分 GC 就不需要碰到更老的对象。 写屏障和卡表 分代 GC 还有一个关键问题:老对象可能引用新对象。 如果下一次只回收 Gen 0,而 GC 不知道某个 Gen 2 对象里正好存着一个指向 Gen 0 对象的引用,就可能把本来还活着的对象误回收。 所以 GC 需要一种增量记录机制。最常见的做法是: 当程序写入对象引用时,顺手执行一个很小的额外动作,这叫写屏障 把对应内存区域标成“这里可能需要重点检查”,这叫卡表 这样 GC 在回收年轻代时,就不必重新扫描整个老年代,而只需要检查被写屏障标脏的那部分区域。 不可能三角 GC 最痛苦的地方在于,高吞吐、低延时、低内存这三个目标经常互相打架。 如果想要低延时,通常就得把更多工作搬到并发阶段,让程序和 GC 尽量同时工作。问题是,并发不是白来的。为了保证并发期间不出错,往往需要更复杂的屏障、更严格的不变量,甚至更多额外空间。 如果想要高吞吐,通常又希望平时路径尽量成本够低。也就是说,分配对象、读取引用、修改引用这些日常动作最好不要成本太高。问题在于,如果日常什么都不做,很多账就得在真正回收时一次性算清,这又容易把停顿做长。 如果想要低内存占用,通常需要更积极地回收、更积极地整理碎片、甚至更积极地把空闲内存还给操作系统。但这些动作本身也要消耗时间和资源。 所以不同 GC,本质上是在回答同一个问题:我到底把成本放在哪里? Satori 的目标 Satori 不是一个完全脱离 .NET 运行时的玩具实验。它直接接在真实运行时接口上,也就是说,它不是纸上谈兵,而是真的在现实约束下重新组织回收方式。 它的目标很明确: 尽量少调参,最好能自动适应工作负载 避免长时间停顿 在现实功能约束下保持完整可用 这里“现实功能约束”很重要。因为一个 GC 如果只是把难题都删掉,那当然容易写得漂亮。但 Satori 并不是这么做的。它仍然要支持内部指针、终结器、弱引用、依赖句柄、可卸载类型、精确根扫描和保守根扫描。 也就是说,Satori 不是回避真实世界场景,而是在真实世界场景下重新组织 GC 的工作。 Page、Region 和代 Satori 的堆组织方式围绕 Page 和 Region 这两个概念展开。 Page:更大的预留单位 Region:Page 内部更小、更适合独立管理的单位 一个大 Page 里可以包含很多个小 Region,而 GC 可以围绕这些 Region 做更细粒度的管理决策。 这里 Region 很重要,因为在 Satori 里,Region 不只是物理切分方式,它还是: 分配单位 线程本地所有权单位 Gen 0 局部回收单位 移动和整理的规划单位 空闲内存归还时的处理单位 很多 GC 虽然也会把堆切成很多小块,但这些小块更多只是方便调度。Satori 不一样,Region 本身就是它的核心抽象。 Satori 在 Page 和 Region 周围维护了不少紧凑的元数据。Page 里会有卡表、Region 映射和更粗粒度的卡组信息;Region 里则会有位图,记录对象的关键状态,例如是否已标记、是否已逃逸、是否被固定。这些元数据决定了 Satori 后面能不能高效地做线程本地回收、逃逸跟踪和局部压缩整理。
阅读全文