这是我参与「第五届青训营」伴学笔记创作活动的第5天。青训营的第五次课程中讲解了自动内存管理、Go内存管理及优化、编译器及静态分析、Go编译器优化等相关知识,在本文中,将针对于课程中关于内存管理及其优化的知识点进行回顾整理。
自动内存管理
在C/C++语言中,内存泄漏是代码的常见错误,其易发生、难调试的特点使得C/C++程序员需要花费很多精力在手动内存管理上。自动内存管理旨在于避免用户的手动内存管理,并使得用户可以专注于实现本身的代码逻辑。
自动内存管理的核心任务为:
- 为新内存对象分配空间,由Mutator(业务线程)负责;
- 找到存活内存对象,并回收死亡对象的内存空间collector(GC线程)负责;
在课程中讲述了两种常见的内存管理方法:追踪垃圾回收、引用计数。
追踪垃圾回收
追踪垃圾回收方法通过判断指针指向对象不可达的方式,判断对象是可以被回收。其由三个流程组成:
- 标记根对象:标记静态变量、全局变量、线程栈等根对象;
- 标记:从根对象出发,通过指针指向关系找到所有可达对象;
- 清理:回收所有不可达对象的内存空间。
在具体的清理过程中,需要根据对象的生命周期制定不同的清理策略,常见的清理策略有以下三种:
- Copying GC:在每次清理时,将存活的对象复制到另外的内存空间中,并将原有整块内存空间释放;
- Mark-sweep GC:使用空闲内存链表管理空闲内存,并不将存活对象进行移动;
- Compart GC:该方法与第一种方法类似,区别在于其将存活对象进行压缩整理在原有内存区域。
该方法又称为分代GC。该方法的出发点基于分代假说,该假说认为很多对象在分配后很快就不使用了。在分代GC方法中,通过记录各对象经历GC的次数,来确定对象的年龄。
对于年轻对象,GC判断该种对象存活数量很少,可以采用Copying GC或Compart GC的方法,将该部分内存进行压缩;
对于老年对象,GC判断该种对象趋于一直活着,进行压缩反复移动开销较大,因此可以采用空闲链表的方法管理该种内存对象。
引用计数
引用计数方法通过将每个对象关联一个引用数目的方式进行管理,当对象的引用数目为0时,内存管理器判断该对象死亡并进行回收。
该方法的优点在于:
- 内存管理的操作被平摊在程序的执行过程中,而不是在一次标记-清除过程中整块进行;
- 该方法的实现方法比较简单,不需要了解运行时的实现细节,通常被用于于C/C++程序的对象管理中。
该方法的缺点在于:
- 需要通过原子操作维护引用计数,开销较大;
- 无法回收环形数据结构;
- 需要在每个对象中存放引用计数,引入了额外的内存开销;
- 当大型对象被释放时,内存释放的密度仍会较大,可能造成业务流程暂停;
Go语言内存管理及优化
内存分块、内存缓存为Go语言内存管理方法中的两个主要部分:
- 内存分块:Go语言内存管理通过调用
mmap()系统调用向操作系统请求一大块的内存,并将大块内存等分为若干子部分mspan。然后,mspan将各自内存被划分为特定长度的小块,用于特定大小的对象分配。- 对于不包含指针的对象,将其分配在
noscan mspan中,并且GC不需要对该种mspan进行扫描; - 对于不包含指针的对象,将其分配在
scan mspan中,GC需要对该种mspan进行扫描;
- 对于不包含指针的对象,将其分配在
- 内存缓存:Go语言内存管理器中包含
mcache,其中管理一组mspan用于快速对象分配。当mcache中的mspan被分配完毕时,其向mcentral请求带有未分配块的mspan;并且,当mspan中没有已分配对象时,msapn会被缓存在mcentral中,而不是立即返回只操作系统。
内存管理优化
在这里,字节跳动针对系统对象分配及Go语言内存管理的特点,对Go语言做出了优化。其出发点在于:
- 在业务系统中对象分配是比较高频的操作,且小对象的占比较高;
- Go语言的内存分配路径较长,比较耗费时间;
因此,字节跳动针对小对象分配做出了如下优化:
- 使得内存管理器绑定一大块内存,称作GAB(goroutine allocation buffer);
- GAB将专用于
noscan类型的小对象(小于128B)分配; - 在GAB中,使用简单的指针递增方式分配对象,分配方法简单高效;
该方法的本质在于,将若干个小对象的分配整合为一个大对象的分配。其主要问题在于,GAB中的小对象未释放将会导致整个GAB无法释放,导致内存占用。
针对该问题的解决方案为,当所有GAB的总大小超过一定阈值时,将GAB中的存活对象整合到另外的GAB中,整合中分配的GAB可以被释放。该方法的本质于分代GC思想相同,即根据对象的生命周期实施不同的内存管理操作。