返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

3.1 概述

发布于 2024-10-12 19:16:03 字数 3423 浏览 0 评论 0 收藏 0

基于官方定义,垃圾回收器基本特征是:并发、非分代、非收缩。

// mgc.go

// The GC runs concurrently with mutator threads, is type accurate (aka precise), 
// allows multiple GC thread to run in parallel. It is a concurrent mark and sweep 
// that uses a write barrier. It is non-generational and non-compacting. 

所谓并发,是指垃圾回收与用户代码同时执行。因指针的存在,回收后内存不能做收缩处理。

另外,内存分配器一章所提及代龄仅用来标记内存块清理状态,而非将对象分成 G0、G1、G2。

标记(mark)和清理(sweep)默认都是并发执行。

通常按 GOMAXPROCS 25% 的 CPU 利用率进行标记工作。

可通过 GODEBUG gcstoptheworld 关闭并发。

主要指标:

1. 尽可能缩短 STW 时间。

2. 单个回收周期(cycle)要小于 10ms。

3. 回收期间,CPU 利用率不超过 25%。

基本步骤:

1. 标记设置(mark setup),STW!

2. 标记(marking),并发。

3. 标记结束(mark termination),STW!

4. 清理(sweep),并发。

并发回收

最初,垃圾回收前提是冻结(stop-the-world)用户代码,以便扫描出不可达对象,进而完成清理操作。

此方法简单、干净,但缺点也很明显。比如说,STW 可能会导致用户算法超时,这对游戏等实时要求比较高的应用很致命。

在多核处理器成为主流的情况下,减少用户逻辑停顿时间,并发执行垃圾回收就成为关键需求。

只是在回收时执行用户逻辑,会让内存处于不稳定状态。已扫描内存或对象可能会因用户逻辑再次改变状态,且时刻有新对象分配。

如此种种,让并发设计变得极为复杂。

内存回收大体可分为标记(mark)和清理(sweep)两步。

前者以根对象为起点,扫描并标记出存活对象,后者则回收所有未标记对象所占内存。

可见,最大麻烦在于并发标记,并发扫描相对容易些。

当前并发设计尚未能消除 STW。

在某些关键点,依然要冻结用户代码,以便相关逻辑进入安全点,启用包括写屏障在内的跟踪机制。

即便如此,并发设计依然大大缩减了冻结时间。

三色标记

扫描内存时,使用三种颜色标记对象状态。

1. 起初,所有对象默认为白色。

2. 扫描,将可达对象标记为灰色,放入队列。

3. 依次从队列提取灰色对象,继续扫描。

* 灰色对象自身转为黑色,表示存活。

* 被灰色所引用对象,同样标记为灰色,放回队列。

4. 扫描结束后,仅剩黑白二色。白色表示待回收。

通过队列实现递归扫描,找出存活(黑色)对象,其余被回收。这就是三色标记原理,大体与扫描流程相对应。

至于用户逻辑在扫描阶段新建的用户对象,直接标记为黑色。

标记操作对于:

a := make([]*int, 1e9)

b := make([]int, 1e9)

性能相差千倍。b 无需深入内部扫描,故而快得多。

所以,对于超大块内存使用要慎重。

比如说,分配一大块 []byte 或 mmap,持有阻止回收。然后,在内部使用 uintptr 二次分配。

写屏障

直观上看,写屏障是编译器在用户逻辑内插入的额外指令。

因三色标记和用户逻辑并发执行,那么已检查过的黑色对象就可能被修改。

假设已扫描的黑色对象内部指针 “突然” 指向一个尚未扫描白色对象。

按三色标记流程,黑色不会再次扫描,如此就导致该白色对象最终被回收,从而引发逻辑错误。

A(黑) 引用 B(灰);B 引用 C(白)。

然后,A 引用 C,B 不再引用 C。

如果没有写屏障,那么 A 不会再次扫描,C 保持白色被回收。

写屏障启用后,对指针的修改会跳转到写屏障指令,以便对其重新标记、扫描。如此,其引用的白色对象会存活下来。

写屏障解决了垃圾回收与用户逻辑并发执行的冲突,有助于减少重新扫描次数,简化和消除了某些复杂机制。

写屏障仅在垃圾回收时启用,通过特定开关进行判断。

正常情况下,用户逻辑不会跳转到这些额外指令,性能不受影响。

控制器

垃圾回收有个大问题需要解决,那就是什么时候启动?

过早或频繁启动,除浪费 CPU 资源外,还会加大用户逻辑停顿时间。

过晚又会导致堆大小膨胀,浪费更多内存。如何在两者间平衡,是个巨大的挑战。

早期版本,以存活对象大小总和的两倍作为触发阈值。此方式过于粗暴,未考虑执行过程中的动态因素。

为此,引入调步控制器,通过收集回收周期内相关数据,以反馈控制算法动态计算触发阈值,适时调整并发执行模式。

从而有效控制垃圾回收执行频率,平衡堆增长和 CPU 利用率。

// mgc.go

// gcController implements the GC pacing controller that determines
// when to trigger concurrent garbage collection and how much marking
// work to do in mutator assists and background marking.
//
// It uses a feedback control algorithm to adjust the memstats.gc_trigger
// trigger based on the heap growth and GC CPU utilization each cycle.

辅助回收

回收阶段,对象分配速度可能远快于回收标记。

这会引发一系列恶果,比如堆恶性扩张,或导致单个回收周期无法结束,以至于垃圾回收机制瘫痪。

此时,暂停用户逻辑,让该线程临时参与辅助回收就十分必要。

此举非但能抑制用户逻辑在短时间内大批量分配内存,还可提升回收效率。

平衡分配和回收,更充分复用内存。

具体做法是,为每个用户线程设置信用额度。

在回收启用时,每次分配将消费与内存大小等量的信用值。

如额度为负,则需参与辅助回收,为后续分配挣得可用数额。

如此,信用额度就为用户逻辑的内存分配行为设定了阶段性上限。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文