这是我参与「第五届青训营」伴学笔记创作活动的第 8 天
前言
本文主要介绍:
- Go内存分配以及内存管理优化
PS:若文章有什么问题,可以在文末进行留言或者私信。
Go内存管理以及优化
1 Go内存分配——分块
目标:
- 为对象在heap上分配内存
- 提前将内存分块(从大到小)
- 调用系统调用
mmap()向OS申请一大块内存,例如4MB - 先将内存划分成大块,例如8KB,称作mspan
- 再继续将大块划分成特定大小的小块,用于按需对象的分配
- 分块如下图所示
- mspan的类型:
- noscan mspan:分配不包含指针的对象——GC不需要扫描
- scan mspan:分配包含指针的对象——GC需要扫描
- 调用系统调用
- 对象分配:根据对象的大小,选择最合适的块
2 Go内存分配——缓存
- TCMalloc:thread caching
- 每个p包含一个mcache用于快速分配,用于为绑定于p上的g分配对象
- mcache管理一组mspan
- 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
- 当mspan中没有分配的对象,mspan会被缓存在mcentral中,而不是立刻释放并归还给OS
最后一点通过图理解更好,
整体缓存的架构图:
3 Go内存管理优化
对于高并发的Go语言来说:
- 对象分配是非常高频的操作:每秒分配GB级别的内存
- 空间较小的对象占比比较高
- Go内存分配比较耗时
- 分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
- pprof:对象分配的函数是最频繁调用的函数之一
官方(字节)的优化方案:Balanced GC
-
每个g都绑定一大块内存(1KB),称作goroutine allocation buffer (GAB)
-
GAB用于noscan类型的小对象分配:< 128B(设定的默认值)
-
使用三个指针维护GAB:base基地址, end结束地址, top当前地址
-
Bump pointer(指针碰撞)风格对象分配:
- 无需和其他分配请求互斥
- 分配动作简单高效
//内存分配中与top有关的算法 if top + size <= end { addr := top top += size return addr } -
由下图可以很好理解GAB的整体结构:
Balanced GC的深入理解:
- 从结构来说,GAB对于Go内存来说是一个大对象
- GAB本质:将多个小对象的分配合并成一次大对象的分配
-
这会导致一个问题:
- 如果GAB结构中只有一个小对象存活,那会使得GAB的对象分配方式会导致内存被延迟释放
-
Balanced GC的方案:移动GAB中的存活对象
- 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
- 原先的GAB可以释放,避免内存泄漏
- 本质:用copying GC的算法管理小对象(根据对象的生命周期,使用不同的标记和清理策略)
- 方案图大致如下:
-
关于性能收益:
- 官方提供的数据显示:使用Balanced GC的方案,能使得高峰期的 CPU usage 降低 4.6%,核心接口时延下降 4.5%~7.7% .
- 其实对于Go语言这种主打高并发的语言来说,这种提升已经非常大了。
心得
通过课程的学习,我收获了go的内存分配两个机制——缓存和分块的相关知识,也懂得了对于高并发的go来说,微小的性能提升,可能使得机器的负担大大降低。通过了解字节官方对go内存管理的优化方案——Balanced GC,我一步一步地搞懂了GAB,从Gab的结构,到分析它的不足之处,最后针对于这个问题官方提出了相应的解决方案,很好地引领我去学习Balanced GC,这样的上课节奏对我来说感觉真的不错!
引用
ppt:高性能 Go 语言发行版优化与落地实践 .pptx - 飞书云文档 (feishu.cn)