创建时间: May 17, 2022 5:10 PM 标签: GC, 内存优化
这是我参与「第三届青训营 -后端场」笔记创作活动的的第6篇笔记。
性能优化的定义见上节,略。
GC
GC:(garbage collection) 1、为新的对象分配内存。 2、找到存活对象。 3、回收死亡对象。 其意义在于不用编码人员不用管理内存生命周期,解决 double-free、use-after-free 两大问题
GC 算法的类别
- SerialGC 暂停业务 用一个回收器回收
- ParallelGC 暂停业务 用多个回收器回收
- ConcurrentGC 并行GC 过程中业务受影响小
GC 算法的评价标准
-
安全性
- 不能回收存活对象
- GC 算法的基本要求
-
吞吐率
- GC 时间占总时间的比率
-
暂停时间
- STW 时间,GC 过程是否影响业务
-
内存开销
- GC 的元数据开销
评价 GC 合规的标准,是否”安全“,开销可以接受。
追踪垃圾回收
GC 的最简单实现:SerialGC 暂停业务,从根对象遍历,所有达对象保留,不可达对象直接回收。
- 根对象:静态变量、全局变量、常量、线程栈等
- 可达对象:从根对象求指针引用图,图上的所有节点视为”可达”
- 清理对象:所有不可达对象
清理策略
多个较小对象可能被分配到同一个内存块中,某些小对象将要被清除时将执行的逻辑被称作清理策略。
-
复制 - Coping GC
- 将存活对象从原本的内存块中复制出来,重新分配,放到新的内存空间中。
-
标记 - Mark-sweep GC
- 标记&视作清除,对内存块中的不可达部分进行标记,不立刻执行清除,直到该部分内存空间需要被分配给新的对象。
- 避免对象复制。
-
压缩 - Mark-compact GC
- 移动&整理,将内存块中的不可达对象清除,存活对象就地压缩到内存块头部。
各策略适用场景稍后有整理,部分内容可能有疏漏欢迎读者补充。
分代 GC
-
对象的年龄:对象从被创建出来之后经历 GC 并存活的次数,类似树的年轮?
-
分代的目的:对不同分代的对象制定不同的 GC 策略,降低 GC 开销。
- 不同代的对象处于不同的堆空间,比如 JVM
分代 GC 策略
-
年轻代
- 属于常规的对象分配
- 由于这部分对象存活很少,可以使用 Coping GC 策略
- GC 吞吐率高
-
老年代
- 对象趋向于一直活着,每次 GC 这部分对象都存活,该对象被反复复制
- 可以采用 Mark-sweep GC 策略
引用计数
!! 个人理解
可能非常不准确:引用计数可以减轻部分 GC 压力,代价是付出额外的内存空间。如果回收超大对象时,仍然会触发 STW,暂停业务。引用计数
-
每个对象自身维持一个计数,用于表示与自己关联的引用数目。
-
对象存活:当且仅当引用数 > 0。
-
优点:
- 内存管理操作被平摊到程序的执行过程中。
-
缺点:
- 可能存在孤岛的互相引用对象。
内存优化
Golang 内存模型
为对象在进程的堆空间上分配内存
内存分块
- syscall mmap 向 OS 申请大块内存
(比如 4MB - 将内存划分为大块 mspan
(比如 8KB - 再将一个 mspan 划分为特定大小的块,用于合适对象的内存分配。
- noscan mspan:分配不包含指针的对象——GC 不扫描该内存
( 非逃逸,视作栈空间 - scan mspan:分配包含指针的对象——GC 需要扫描
对象分配:
根据对象大小,匹配最合适的内存块。
内存分配部分总结:对整块内存申请,按使用量进行分配。
GBA 方案(goroutine allocation buffer)
为每个 g 绑定一块内存(1KB),用于分配 noscan 类型的小对象 < 128B。
优化方案来自数据统计:业务项目中该大小区间被创建的对象非常多 使用三个指针维护 base、end、top,利用 P 独享性质,完成无须互斥的内存分配(指针碰撞)。
代价:增加了内存延迟释放的问题?
解决方案:当 GBA 总大小超过一定阈值,将存活对象复制到另分配的 GBA 对象中(合并),释放原本 GBA 所占的内存空间。
本质:用 copyingGC 算法管理小对象
什么叫延迟释放?
对象如果未被引用则在下一次 GC 时内存空间会被回收,但引入 GBA 后,如果 GBA 中有小对象仍然存活,则整个 GBA 所占内存空间不会被立即释放,需等该小对象生命周期结束,这样的问题成为延迟释放。