上卷 程序设计
中卷 标准库
- bufio 1.18
- bytes 1.18
- io 1.18
- container 1.18
- encoding 1.18
- crypto 1.18
- hash 1.18
- index 1.18
- sort 1.18
- context 1.18
- database 1.18
- connection
- query
- queryrow
- exec
- prepare
- transaction
- scan & null
- context
- tcp
- udp
- http
- server
- handler
- client
- h2、tls
- url
- rpc
- exec
- signal
- embed 1.18
- plugin 1.18
- reflect 1.18
- runtime 1.18
- KeepAlived
- ReadMemStats
- SetFinalizer
- Stack
- sync 1.18
- atomic
- mutex
- rwmutex
- waitgroup
- cond
- once
- map
- pool
- copycheck
- nocopy
- unsafe 1.18
- fmt 1.18
- log 1.18
- math 1.18
- time 1.18
- timer
下卷 运行时
源码剖析
附录
3.1 概述
基于官方定义,垃圾回收器基本特征是:并发、非分代、非收缩。
// 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论