Go 内存管理及优化
这是我参与「第五届青训营」伴学笔记创作活动的第4天
1.自动内存管理(Auto memory management)
背景
动态内存:malloc()
自动内存管理:运行时系统管理动态内存。
保证内存使用正确性和安全性:
double-free problem:同一个指针free两次。只要free一个指向堆内存的指针都有可能产生可以利用的漏洞。
use-after-free problem:在free之后使用。
三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
基本概念: - Mutator:业务线程,分配新对象,修改对象指向关系。
- Collector:GC线程,找到存活对象,回收死亡对象的内存空间。
- Serial GC:只有一个collector
- Parallel GC:支持多个collectors同时回收的GC算法
- Concurrent GC:mutator(s)和collector(s)可以同时执行 (必须感知对象指向关系的改变)
Tracing garbage collection——追踪垃圾回收(1)
- 对象被回收条件:指针指向关系不可达的对象
- 标记根对象:
- 静态变量、全局变量、常量、线程栈等
- 标记:找到可达对象
- 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
- 清理:所有不可达对象
- 将存活对象复制到另外内存空间(Copying GC)
- 将死亡对象的内存标记为“可分配”(Mark-sweep GC)
- 移动并整理存活对象 (Mark-compact GC)
- 根据对象的生命周期,使用不同的标记和清理策略
Generational GC——分代GC
- 年龄:经历过GC的次数
- 年轻代:
- 常规分配
- copying collection
- GC吞吐率高
- 老年代:
- 对象趋向于一直活着,反复复制开销大
- 采用mark-swaap collection
Reference couting——引用计数(2)
- 每个对象都有一个与之关联的引用计数
- 对象存活的条件:当且仅当引用计数大于0
- 优点
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解runtime的实现细节
- 缺点
- 维护引用计数的开销大
- 无法回收环形数据结构
- 内存开销:每个对象都引入额外内存空间存储引用数目
- 回收内存时依然可能引发暂停
2.Go内存管理及优化
Go内存分配
分块
- 目标:为对象在heap上分配内存
- 提前将内存分块
- 调用mmap()向OS申请一大块内存
- 将大内存分成大块,mspan
- 大块内存分配成特定块,用于对象分配
- noscan mspan:不包含指针对象
- scan mspan:包含指针对象
- 根据对象大小,分配特定块
缓存——多级缓存
Go内存管理优化
问题:
- 对象分配高频
- 小对象占大多数
- 内存分配消耗CPU资源
优化——Balanced GC
- 每个g绑定一块大内存(1kb),goroutine allocation buffer(GAB)
- GAB用于noscan类型的小对象分配: <128B 细节
- GAB对Go内存管理来说是一个大对象
- 本质:将多个小对象的分配合并为一次大对象分配
- 问题:内存延迟释放
- 方案:移动GAB中存活的对象(超过阈值,复制,释放、copying GC)
3.编译器优化思路
基本介绍
编译器结构
静态分析
不执行代码,推导程序行为
数据流和控制流
过程内和过程间
过程间分析存在问题
- 通过数据流分析得知i具体类型
- 根据i的类型,产生新的控制流
- 数据流和数据流同时分析
4. Go编译器优化
背景
特点:用户无感知,通用性优化 现状:采用优化少,编译时间短 思路:用时间换效率 Beast mode
函数内联(inline)
类似于C++的内联函数。
优点:消除函数调用开销,将过程间分析转换为过程内分析
缺点:函数体变大,Go镜像大
策略:调用和被调函数规模
逃逸分析
分析代码中指针的动态作用域 大致思路
-
从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数;
- 传递给全局变量;
- 传递给其他的 goroutine;
- 传递给已逃逸的指针指向的对象;
-
则指针 p 逃逸出 s,反之则没有逃逸出 s.