这是我参与【第五届青训营】伴学笔记创作活动的第4天
自动内存管理
相关概念
- Mutator: 业务线程,分配新对象,修改对象指向关系
- Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
- Serial GC: 只有一个 collector
- Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法
-
Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法
评价GC算法
-
安全性(Safety):不能回收存活的对象是基本要求
-
吞吐率(Throughput):花在GC上的时间
-
暂停时间(Pause time):stop world(STW)业务是否感知
-
内存开销(Space overhead):GC 元数据开销
两种垃圾回收算法
- 追踪垃圾回收(Tracing garbage collection)
- 引用计数(Reference counting)
追踪垃圾回收
-
对象被回收的条件:指针指向关系不可达的对象
-
标记根对象
- 静态变量、全局变量、常量、线程栈等
-
标记:找到可达对象
- 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
-
清理:所有不可达对象(类似JVM中的GC算法)
- 标记-复制:将存活对象复制到另外的内存空间(Copying GC)
(下图左右两块分别为两块内存区域)
- 将死亡对象的内存标记为“可分配”(Mark-sweep GC)
把死亡对象的内存空间用一个FreeList管理起来,在下一次分配时直接在FreeList中找一个区域进行分配(如下图白色区域)
> #### 什么是FreeList?
>
> `FreeList`本质上还是个`LinkedList`,和`LinkedList`的区别:
>
> - `FreeList`没有`Next`属性,所以不是用`Next`属性存放下一个节点的指针的值。
> - `FreeList`“相当于使用了`Value`的前8字节”(其实就是整块内存的前8字节)存放下一个节点的指针。
> - 分配出去的节点,节点整块内存空间可以被复写(指针的值可以被覆盖掉)
> - `FreeList`一个节点最小为8字节Byte
- 标记-整理:移动并整理存活对象(Mark-compact GC)原地整理对象
根据对象的生命周期,使用不同的标记和清理策略
分代GC
-
很多对象在分配出来后很快就不再使用了,即存活时间短
-
每个对象都有年龄:经历GC的次数
-
目的:对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
-
不同年龄的对象处于heap的不同区域
-
年轻代(Young generation)
- 常规的对象分配
- 由于存活对象很少,可以采用 copying collection
- GC 吞吐率很高(指在GC时具体花了多少时间进行GC)
-
老年代(Old generation)
- 对象趋向于一直活着,反复复制开销较大
- 可以采用 mark-sweep collection
-
引用计数
-
每个对象都有个与之关联的引用数目
-
对象存活的条件:当且仅当与引用数大于0
-
优点
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解runtime的实现细节:C++智能指针(smart pointer)
-
缺点
- 维护引用计数的开销比较大:通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形的数据结构
- 内存开销:每个对象都引入的额外内存空间储存引用数目
- 回收内存时依然可能存在暂停
Go内存管理及优化
Go 内存管理分配
分块
-
目标:为对象在heap上分配内存
-
提前将内存分块
- 调用系统调用mmap() 向 OS 申请一大块内存,例如4MB
- 先将内存划分成大块,例如 8 kb,称作对象分配 mspan
- 再将大块继续划分程特定大小的小块,用于对象分配
- noscan mspan:分配不包含指针的对象 —— GC不需要扫描
- scan mspan:分配包含指针的对象 —— GC需要扫描
-
对象分配:根据对象的大小,选择最合适的块返回
缓存
Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存。内存分配的路线图如下。
- TCMalloc:thread caching
- 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
- mcache 管理一组 mspan
- 当 mcache 中的 mspan 分配完毕,向mcentral 申请带有未分配块的 mspan
- 当 mspan 中没有分配的对象,mspan 会被缓存在 mcentral 中,而不是立刻释放并归还给 OS
Go 内存管理优化
- 对象分配时非常高频的操作:每秒分配 GB 级别的内存
- 小对象占比较高
-
Go 内存分配比较耗时
- 分配路径长:g -> m -> p -> mcache -> msapn ->memory block -> return pointer
- pprof:对象分配的函数是最频繁调用的函数之一
字节的优化方案:Balanced GC
- 每个 g 都绑定一大块内存(1kb),称作 goroutine allocation buffer(GAB)
- GAB 用于 noscan 类型的小对象分配:< 128 B
- 使用三个指针维护 GAB:base,end,top
-
Bump pointer(指针碰撞)风格对象分配
- 无须和其他分配请求互斥
- 分配动作简单高效
-
GAB 对于 Go 内存管理来说是一个大对象
-
本质:将多个小对象的分配合并成一次大对象的分配
-
问题:GAB 的对象分配方式会导致内存被延迟释放
-
方案:移动 GAB 中存活的对象
- 当 GAB 总大小超过一定阈值时,将 GAB 中存活的对象复制到另外分配的 GAB 中
- 原先的 GAB 可以释放,避免内存泄漏
- 本质:用 copying GC 的算法管理小对象(根据对象的生命周期,使用不同的标记和清理策略)
Balanced GC 性能收益
-
高峰期 CPU usage 降低 4.6%,核心接口时延下降 4.5% ~ 7.7%
总结: GoLang中内存管理以及垃圾回收策略,与JVM有很大的相似之处,对JVM熟悉的同学能更好的理解GoLang的机制,同时掌握内存管理以及垃圾回收相关知识能更好的帮助我们使用这个语言