一、Go 内存分配 - 分块
-
基本原理
- 内存申请:Go 程序启动时,会通过系统调用
mmap()向操作系统申请一大块内存,例如 4 MB。这是内存分配的基础。 - 划分大块:将申请到的大内存划分成较大的块,如 8 KB 大小的块,这些块被称为
mspan。mspan是 Go 内存管理中的重要结构。 - 细分小块:每个
mspan会进一步被划分成更小的特定大小的块,用于实际的对象分配。这些小块的大小根据对象的大小需求而定。
- 内存申请:Go 程序启动时,会通过系统调用
-
mspan类型-
noscan mspan- 用于分配不包含指针的对象。在垃圾回收(
GC)过程中,这些对象不需要被扫描,因为它们不包含指针,不会引起引用关系的追踪问题。
- 用于分配不包含指针的对象。在垃圾回收(
-
scan mspan- 用于分配包含指针的对象。在
GC时,需要扫描这些对象,以处理对象之间的引用关系,确保内存的正确回收和对象的存活判断。
- 用于分配包含指针的对象。在
-
-
对象分配策略
- 根据对象的大小,从已划分好的小块中选择最合适的块进行分配。这种策略可以减少内存碎片,提高内存利用率。
二、Go 内存分配 - 缓存
-
TCMalloc机制- 线程缓存:在 Go 中,采用了类似
TCMalloc(Thread - Caching Malloc)的机制。每个处理器(p)都有一个本地的mcache用于快速分配内存。 - 协程分配:
mcache用于为绑定在该处理器上的协程(g)分配对象。当协程需要分配内存时,首先会在本地的mcache中查找可用的mspan。
- 线程缓存:在 Go 中,采用了类似
-
mspan缓存管理mcache与mcentral的交互:当mcache中的mspan分配完毕后,会向mcentral申请带有未分配块的mspan。- 缓存机制:当
mspan中没有分配的对象时,mspan会被缓存在mcentral中,而不是立即释放并归还给操作系统。这种缓存机制可以减少内存分配和释放的开销。
三、Go 内存管理优化
-
堆内存分配的尺寸分布
- 分析内存分配频率:通过分析堆内存分配的尺寸分布,可以了解不同大小的对象在内存分配中的频率。通常会发现小对象的分配频率较高。
- 优化方向:由于小对象占比较高,优化小对象的分配和管理可以显著提高内存管理的效率。
-
对象分配的性能问题
- 高频操作:对象分配在 Go 程序中是非常高频的操作,每秒可能会分配 GB 级别的内存。
- 小对象居多:小对象在内存分配中占比较高,而小对象的分配和管理可能会带来更多的开销。
- 分配路径长:Go 内存分配的路径较长,涉及到协程(
g)、处理器(p)、mcache、mspan等多个层次的查找和分配操作,这导致了内存分配比较耗时。 pprof分析:pprof工具可以用于分析对象分配的性能,通常会发现对象分配函数是最频繁调用的函数之一,这也是优化的重点方向。
四、Balanced GC(平衡垃圾回收)
-
goroutine allocation buffer (GAB)- 基本概念:每个协程(
g)都绑定一大块内存(如 1 KB),称为GAB。GAB主要用于分配小于 128 B 的noscan类型小对象。 - 指针管理:使用
base、end、top三个指针来维护GAB。采用Bump pointer(指针碰撞)风格进行对象分配,这种方式简单高效,不需要与其他分配请求互斥。
- 基本概念:每个协程(
-
Balanced GC的问题与解决方案-
问题
GAB本身对于 Go 内存管理来说是一个对象,它将多个小对象的分配合并成一次这个对象的分配。这种方式可能会导致内存被延迟释放。
-
解决方案
- 对象移动:当
GAB的总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中,原GAB可以释放,避免内存泄漏。 - 算法本质:这种方式本质上是采用
copying GC(复制式垃圾回收)的算法来管理小对象。根据对象的生命周期,使用不同的标记和清理策略,提高垃圾回收的效率。
- 对象移动:当
-
五、Balanced GC - 性能收益
-
性能提升数据
-
通过采用
Balanced GC,在高峰期可以使CPU usage降低 4.6%,核心接口时延下降 4.5% - 7.7%。这表明Balanced GC在优化内存管理和垃圾回收方面取得了显著的性能提升。
-
通过这些机制和优化策略,Go 语言在内存管理和垃圾回收方面能够在兼顾易用性的同时,实现高效的性能表现。