Go Memory
内存分配⌗
内存分配过程⌗
针对待分配对象大小的不同有不同的分配逻辑:
- (0, 16B)且不包含指针的对象: Tiny分配
- (0, 16B)且包含指针的对象: 正常分配
- [16B, 32KB] : 正常分配
- (32KB, ∞): 大对象分配
以申请size为n的内存为例,分配步骤如下:
- 获取当前线程的私有缓存mcache
- 根据size计算出适合的class的ID
- 从mcache的alloc[class]链表中查询可用的span
- 如果macache没有可用的span, 则从mcentral申请一个新的span加入mcache
- 如果mcentral中也没有可用的span, 则从mheap中申请一个新的span加入mcentral
- 从该span中获取空闲对象地址并返回
小结⌗
- Go程序启动时申请一大块内存, 并划分成span、bitmap、arena区域
- arena区域按页划分成一个个小块
- span管理一个或多个页
- mcentral管理多个span供线程申请使用
- mcache作为线程私有资源, 资源来源于mcentral
垃圾回收⌗
常见垃圾回收算法⌗
-
引用计数 对每个对象维护一个引用计数, 当引用改对象的对象被销毁时,引用计数减1, 当引用计数器为0时回收改对象 优点: 对象可以很快被回收,不会出现内存耗尽或达到某个阈值时才回收 缺点: 不能很好地处理循环引用, 而且实时维护引用计数也有一定代价
-
标记-清除 从根变量开始遍历所有引用对象, 引用的对象标记为“被引用”, 没有标记的对象被回收 优点: 解决了引用计数的缺点 缺点: 需要”Stop The World“
-
分代收集 按照对象生命周期的长短划分不同的代空间, 生命周期长的放入老生代, 短的放入新生代, 不同代有不同的回收算法和回收频率 优点: 回收性能好 缺点: 算法复杂
Go垃圾回收⌗
“标记-清除”算法原理⌗
内存标记⌗
在mspan
的数据结构中, bitmapallocBits
表示每个内存块的使用情况, bitmapgcmarkBits
用于标记内存块被引用的情况
在标记阶段对每块内存进行标记, 有对象引用的内存标记为1,没有引用的保持为0(default)
allocBits
和gcmarkBits
的数据结构完全一样, 标记结束后进行内存回收, 回收时将allocBits指向gcmarkBits, 代表标记过的内存才是存活的, gcmarkBits会在下次标记时重新分配内存
三色标记法⌗
三色主要是为了对应gc过程中对象的三种状态:
- 灰色: 对象还在标记队列中等待
- 黑色: 对象已被标记, gcmarkBits对应的位为1(本次不会被清理)
- 白色: 对象未被标记, gcmarkBits对应的位为0(本次会被清理)
Stop The World⌗
在gc过程中, 需要控制内存的变化, 否则在回收过程中指针传递会引起内存引用关系变化 STW时间的长短直接影响了应用的执行
垃圾回收优化⌗
写屏障(Write Barrier)⌗
STW的目的是防止GC扫描时内存变化而停止goroutine, 而写屏障就是让goroutine与GC同时运行的手段. 虽然写屏障不能完全消除STW,但是可以大大缩短STW的时间
辅助GC(Mutator Assist)⌗
在GC过程中, 如果goroutine需要分配内存, 那么改goroutine会参与以部分GC的工作
GC的触发时机⌗
- 内存分配量达到阈值触发GC
- 定期触发GC
- 手动触发
GC性能优化⌗
GC性能与对象数量负相关
- 减少对象分配: 对象复用或使用大对象组合多个小对象
- 内存逃逸也会加重GC负担
逃逸分析⌗
逃逸分析(escape analysis)是指编译器决定内存分配的位置, 不需要程序员指定 在函数中申请一个新的对象:
- 如果分配在栈中, 则函数执行结束后可自动将内存回收
- 如果分配在堆中, 则函数执行结束后可交给GC处理
逃逸策略⌗
在函数中申请一个闲的对象, 编译器会根据该对象是否被函数外部引用来决定是否逃逸:
- 如果函数外部没有引用, 则优先放入栈中
- 如果函数外部存在引用, 则优先放入堆中
- 对于仅在函数内部使用的对象, 也有可能放到堆中, 比如内存过大超过栈的大小
逃逸场景⌗
- 指针逃逸 Go返回了局部变量的指针
- 栈空间不足 当栈空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中
- 动态类型逃逸 如果函数中使用了动态类型参数, 编译期间很难确定参数类型, 也会产生逃逸
- 闭包引用对象逃逸 闭包中的局部对象由于闭包的引用, 产生逃逸
小结⌗
- 栈上分配内存比在堆中分配内存有更高的效率
- 逃逸分析的目的是决定分配到栈还是堆
- 逃逸分析在编译阶段完成