这是我参与「第五届青训营」伴学笔记创作活动的第2天
1.自动内存管理
1.1相关概念
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC线程,找到存活对象,回收死亡对象的内存空间
- Serial GC:只有一个collector
- Parallel GC:支持多个collectors同时回收的GC算法
- Concurrent GC:mutator(s)和collector(s)可以同时执行
注意:Collector GC的时候,Collectors必须感知对象指向关系的改变。如下图所示:
当在GC前有o和a两个对象并且o指向了a,如①所示,在GC过程中,标记了o和a两个对象是存活的,如②所示,而在发生回收之前又有一个对象b被对象o指向,但是o和a已经被标记了,所以会造成这个b对象没有被标记到从而可能被回收的错误,如③所示。所以在Concurrent GC的时候,Collectors必须感知对象指向关系的改变
1.2 追踪垃圾回收
进行垃圾回收的时候可分为三个步骤,第一步标记根对象,第二步找到可达对象并标记,第三步清理所有不可达对象
①标记根对象:将静态变量、全局变量、常量、线程栈等以后会用的变量所指向的对象标记为存活的,如下图
② 找到根对象出发所有可达的对象,也就是求指针指向关系的传递闭包
③清理不可达的对象,这里设计几种清理策略
- Copying GC:将所有存活的对象复制到一块新的内存中去,并清理原来的内存空间
- Mark-sweep GC:将要被清理的对象的内存标记为“可分配”,这样当下一次内存申请的时候发现该对象被标记成“可分配”,就可以使用这一块内存空间
- Mark-compact GC:在原本的内存空间上进行整理,将存活的对象整理到该内存空间的某一块地方
1.3分代GC
每个对象都有年龄,当一个对象经历了一次GC并且没有被回收,它的年龄将会+1,在内存中分为了新生代(Young Generation)和老年代(Old Generation)两个区域,针对不同年龄的对象将其放入不同的区域中
此处需要提及分代假说,分代假说是指每次GC后大多数的对象的声明周期都很短,例如在一个方法中new的对象,方法结束之后该对象就应该被回收了。
对于不同的区域应该灵活的选用不同的GC算法。
新生代中:
- 常规的对象分配
- GC吞吐率高
- 由于存活对象很少,可以采用copying collection
老年代
- 对象一直存活,反复复制开销大
- 可以采用mark-sweep collection
1.4引用计数法
在引用计数法中,每个对象都有一个与之关联的引用数目,当被一个对象被另一个对象引用的时候,其数目+1.
对象存活的条件是当且仅当引用数大于0
如图所示,当前对象为o,指针p指向该对象,指针q指向空,此时o的引用数是1
接下来把q指向了p,此时就有两个指针引用该对象,其数目为2
优点
- 内存管理的操作被平坦到程序执行的过程中
- 内存管理不需要了解runtime的实现细节
缺点
- 维护引用技术的开销大:通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构,因为该环中每个引用数都不小于1
- 内存开销:每个对象都引入的额外内存空间存储引用数目
- 内存回收时可能引发暂停
2.Go 内存分配
2.1内存分配--分块
- 目标:为对象在heap上分配内存
- 提前将内存进行分块,当请求一个对象的空间时,选择一个与该需求空间最接近的内存空间,快速地进行内存分配
- 步骤:
-
调用系统调用mmap()向OS申请一大块内存,例如是4MB
-
现将内存划分成大块,例如8KB,称作mspan
-
再讲大块继续划分成特定大小的小块,用于对象分配
-
noscan mspan:分配不包含指针的对象--GC不需要扫描
-
scan mspan:分配包含指针的对象--GC需要扫描
-
2.2内存分配--缓存
- 每个goroutine都绑定了一个p,p包含了一个mcache用于快速分配
- 在mcache中管理着一组mspan
- g请求分配内存的时候慧聪mcache中选择一个合适的内存大小用于分配
- 当mcache中的mspan分配完毕后,会向mcentral申请带有未分配块的mspan,然后将其放入mspan中,并分配一个合适的内存大小
- 当mspan中没有分配对象时,mspan会被mcache缓存在mcentral中,而不是立刻释放并归还给OS
2.3内存管理优化
对象分配是非常高频的操作,在大项目中每秒会分配GB级别的内存,在这些对象中小对象占比较高。而由上文可知GO内存分配的路径比较长,g -> m-> p -> mcache -> mspan -> memory block -> return pointer会消耗非常多的时间。所以可以提出一个优化,优化方案的名字叫做Balanced GC。
- 在Balance GC中,每个g都绑定了一块大内存(1KB),称作 goroutine allocation buffer(GAB)
- GAB用于noscan类型的小对象分配,小对象是指大小 < 128 B
- GAB使用三个指针进行维护:base(记录GAB的头地址),top(使用到的地址),end(尾地址)
- 使用指针碰撞锋哥(Bump pointer)进行对象分配,即当当前对象的大小可以由GAB分配的时候,就将top的地址返回,并将top移动相应的大小的位置。优点是无需和其他分配请求互斥,分配动作简单高效
GAB的示意图:
申请空间的示意图:
GAB对于go内存管理来说是一个大对象,本质上是将多个小对象的分配合并成一次大对象的分配,仍然是走mcache和mspan那一套。
!!存在的问题:GAB的对象分配会导致内存被延迟释放。!!
例如在一个1KB大小的GAB中有一个 8B 大小的对象,此时在GC的时候就会认为该整个GAB都是存活的,造成了资源的浪费。
解决方案 使用copying GC算法管理小对象。当GAB的总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中,原先的GAB可以释放,避免内存泄漏