这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
自动内存管理
自动内存管理需要完成以下三个目标:为新的对象分配空间、找到存活的对象、回收死亡对象的空间
在自动内存管理中我们有部分概念需要了解:
-
mutator: 业务线程,分配新的对象(创建新的对象),修改对象指向关系
-
collector: GC线程,找到存活的对象,回收已死亡对象的内存空间(指向无法到达,或者引用计数为0)
-
serial GC: 只有一个collector, 此时我们进行垃圾回收操作会暂停其他业务。
-
Parallel GC: 并行的有多个GC,但是还是会对其他业务线程造成影响
-
concurrent GC: mutator和collector同时执行。但是存在在collector执行期间修改对象指向的情况。
我们评价GC有四个方面:
-
1、安全性,不能回收活的对象
-
2、吞吐率。gc时间越短越好
-
3、暂停时间。业务是否感知
-
4、内存开销,GC元数据的开销
追踪垃圾回收
指针指向关系不可达的对象,即可回收此空间。
追踪垃圾回收有三个步骤:
-
标记根对象: 静态变量,全局变量,常量,线程栈。通常来说对象中如果是包含指针的,那么就是根对象。
-
标记可达对象:指针指向关系的传递闭包。从根对象出发找到所有可达的对象。
-
清理,所有不可达的对象。
而在清理的过程中,我们也有不同的方法:
-
将存货对象复制到另外的内存空间(copying GC)
-
将死亡对象的内存标记位可分配(mark_sweep GC)
-
移动并整理存活的对象(mark-compact GC)
至于这三种方法如何选取,还是要根据对象的生命周期进行判断。例如对于小的变量,我们确实可以使用copying GC。而对于存储空间大的对象,我们可以使用标记清除法。毕竟移动一大块内存还是较为繁琐的。
Generational GC(分代GC)
-
分代假说:大部分的对象总是会快速的死去
-
很多对象在分配出来之后很快就不用了
-
每个对象都有年龄: 经历过GC的次数
-
对年轻和老年的对象,定制不同的GC策略,降低整体的内存管理的开销
年轻代:
- 常规的对象分配
- 由于存货的对象很少,可以采用copying gc
- gc的吞吐率很高
-
不同年龄的对象处于heap的不同位置
- 对象趋于一直存货,反复复制开销很大
- 可以采用marksweep
引用计数
- 每个对象都有一个与之关联的引用数目
- 对象只有在引用数大于0的情况下才算存货。
内存管理的操作被平摊到程序的执行过程中,内存管理不需要了解runtime的细节,这点可以类比与c++的智能指针。还有python的垃圾回收机制也是引用计数的。
但是,引用计数虽然在代码上十分的简洁。但是维护计数的开销较大,他需要通过原子操作保证对引用计数操作的原子性与可见性,这又是一个十分频繁的操作,这就很吃性能了。
而且,他还无法回收环形的数据结构,因为首尾相连,我们的引用计数不可能为0。每个对象都需要引入额外的内存空间存储引用数目,回收内存时可能会引发暂停。这都是引用计数存在的缺点。
Go的内存分配
在heap上分配内存
-
提前将内存分块
- 调用系统的mmap函数,向os申请内存。
- 将内存划分为大块,称为mspan
- 再将大块的继续划分为特定的大小,用于对象的分配
- noscan mspan分配不包含指针的对象 (GC不需要扫描)
- scan mspan分配包含指针的对象(GC需要扫描)
-
根据对象的大小,选择最合适的块返回
-
Go的对象分配是非常高频的操作,每秒分配GB级的内存
-
小对象占比高
-
Go内存分配较为耗时
- 分配路径长: g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- 对象分配的函数时调用最为频繁的函数之一