这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
如果有错误和其他意见,麻烦留言指正💖
本文是Go内存管理及优化的整理总结
TCMalloc
概述
Go的内存分配基于TCMalloc(Thread Cache Malloc)。虽然随着版本迭代,Go的内存管理与TCMalloc的差异不断扩大,但其核心思想是类似的,因此为了更好理解Go的内存管理,需要简单了解一下 TCMalloc。
TCMalloc 有两大特点分块和缓存。
就像它的名称一样,TCMalloc的核心就是为每个线程(Thread)分配一块缓存(Cache) 。
这样做有两点好处:
-
只在预分配缓存时进行系统调用,后续线程申请小内存时直接从缓存分配,不用经过系统调用。而系统调用的开销是很大的。
为什么系统调用相对于普通函数调用的开销大?
我的理解是系统调用涉及用户态和内核态切换,需要保存寄存器信息,还有页表切换,这其中还会导致cpu缓存失效等等影响。
而普通函数调用只涉及压栈的操作。
-
我们知道多线程并发访问同一块内存区域时需要加锁,而TCMalloc为每个线程单独分配一块缓存,是不同的地址空间,因而就无需加锁,减少了锁的开销。
《详解Go语言的内存模型及堆的分配管理》这篇文章将快速分配内存分为三个层次:
- 第一层次:引入虚拟内存,让内存的并发访问的粒度从多进程级别,降低到多线程级别。
- 第二层次:预分配缓存,减少系统调用开销。
- 第三层次:多个线程同时申请小内存时,从各自缓存分配,无需加锁,把内存并发访问的粒度进一步降低了。
个人感想:计算机体系中处处可以看到缓存的身影,我觉得这是种通过复用来提高性能的思想,像各种池,如连接池本质也是一种缓存。平时可以思考一下如何利用这种缓存的思想来提高程序的性能。
名词概念简介
这些数据结构的设计体现了分块的思想。
-
Page
与操作系统一样,TCMalloc堆内存的管理也是以页为单位。但是两者的大小不一定相同。一般在x64下 Page 为8kB。
-
Span
Span 是一组连续的 Page ,是TCMalloc 中内存管理的基本单位。
-
ThreadCache
ThreadCache是每个线程各自的 Cache,一个 Cache 包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
-
CentralCache
CentralCache是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同。
当ThreadCache的内存块不足时,可以从CentralCache获取内存块;当ThreadCache内存块过多时,可以放回CentralCache。
由于CentralCache是共享的,所以它的访问是要加锁的。
-
PageHeap
PageHeap是对堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span。
当CentralCache的内存不足时,会从PageHeap获取空闲的内存Span,然后把1个Span拆成若干内存块,添加到对应大小的链表中并分配内存;当CentralCache的内存过多时,会把空闲的内存块放回PageHeap中。
Go 内存分配
概述
基于 TCMalloc,核心也是分块和缓存。
名词概念简述
-
Page
与 TCMalloc 的 Page 相同
-
mspan
与 TCMalloc 的 span 类似,它是golang内存管理中的基本单位。根据对象是否包含指针,将内存块分为scan 和 noscan两种。
-
mcache
与 ThreadCache 类似,区别在于TCMalloc是每个线程拥有1个ThreadCache,Go是每个 P 拥有一个 mcache。因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问。
P(processor)代表上下文,可以认为是cpu
M(work thread)代表工作线程
G(groutine)代表协程
-
mcentral
与CentralCache类似,是线程共享的缓存。
当 mcache 中的 mspan 分配完毕,向 mcentral申请带有未分配块的mspan。
当 mspan中没有分配的对象,mspan 会被缓存在 mcentral中, 而不是立刻释放并归还给 OS。
-
mheap
与PageHeap类似,是堆内存的抽象。
Go 内存管理的问题
-
对于很多线上业务,对象分配是非常高频的操作:每秒GB级别
-
小对象占比比较高
-
Go 内存分配比较耗时
- 分配路径长: g ->m -> p -> mcache -> span -> memory block -> return pointer
- 通过 pprof 工具观察到对象分配函数调用频繁。
Balanced GC
Balanced GC 是字节跳动针对上述问题的优化方案。
特点
- goroutine allocation buffer(GBA):为每个 g 都绑定一大块内存(1 KB)
- GAB 用于 nosan 类型的小对象(<128B)分配
- bump-the-pointer :指针碰撞风格的对象分配,高效简单
基于之前提到的小对象频繁分配的问题,Balanced GC 做的本质就是将多个小对象的分配合并成一次大对象(GBA)的分配。
问题
GAB 中只要有一个小对象存活,这个GAB就不会被内存释放,导致内存的延迟释放。
解决方案
使用copying GC 算法解决,当GAB 的大小超于一定阈值时,将GAB中存活的对象拷贝到另外分配的GAB中,然后释放原先的GAB。
参考及推荐阅读
《Go 语言原本》7.1内存分配设计原则 主要是源码分析
《详解Go语言的内存模型及堆的分配管理》 深入浅出非常详细得讲解了内存管理。