这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
本堂课主要内容是Go语言的自动内存管理、内存分配以及编译器优化,下面主要是我个人听课时的一些笔记。
个人笔记
Go自动内存管理
-
自动内存管理是对于动态内存而言的
如程序在运行时根据需求动态分配的内存:
malloc() -
自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存
-
避免手动内存管理,专注于实现业务逻辑
-
保证内存使用的正确性和安全性:double-free problem, use-after free problem
-
-
三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
-
相关概念
Mutator: 业务线程,分配新对象,修改对象指向关系
Collector: GC线程,找到存活对象(内存标记),回收死亡对象的内存空间(内存回收)
Serial GC: 只有一个collector
Parallel GC: 支持多个collectors 同时回收的GC算法
Concurrent GC: mutator(s)和collector(s)可以同时执行
-
concurrent GC的难点在于,collectors必须要感知到对象指向关系的改变
GC过程中新添加的对象,也需要被标记
- 评价GC算法
- 安全性(Safety): 不能回收存活的对象 基本要求
- 吞吐率(Throughput):
- 暂停时间(Pause time): stop the world (STW) 业务是否感知
- 内存开销(Space overhead) GC元数据开销
- 追踪垃圾回收(Tracing garbage collection)
- 引用计数(Reference counting)
-
-
追踪垃圾回收(Tracing garbage collection)
对象被回收的条件:指针指向关系不可达的对象
- 标记跟对象
- 标记:找到可达对象
- 清理:所有不可达对象
清理不可达对象时会有三种策略
-
Copying GC
将对象复制到另外的内存空间
-
Mark-Sweep GC
使用free-list管理空闲内存
-
Mark-Compact(标记-压缩) GC
原地整理对象
-
分代GC(Generational GC)场景
分代假说:most objects die young
年轻代采用copying GC,因为其存活时间短,所以使用copying GC不会特别耗时
老年代采用Mark-sweep GC,对象存活时间长,反复复制开销较大
若发现碎片较多,可以采取一次Compact(压缩)
-
引用计数(Reference counting)
计算该对象被多少个指针“引用”,即被多少个指针指向
优点:
-
内存管理的操作被平摊到程序执行过程中
-
内存管理不需要了解runtime的实现细节,如C++的智能指针(smart pointer)
追踪垃圾回收(Tracing garbage collection)需要和runtime耦合的更紧密
缺点:
-
维护引用计数的开销较大: 通过原子操作保证对引用计数操作的原子性和可见性
-
无法回收环形数据结构
通过weak reference解决这个问题
-
内存开销: 每个对象都引入的额外内存空间存储引用数目
-
回收内存时依然可能引发暂停,如源头节点释放后,会引起很多内存的回收,这个过程中仍然可能引发暂停
-
Go内存分配
-
分块:为对象在heap上分配内存
提前将内存分块
根据对象的大小,选择最合适的块返回
-
缓存
-
优化原因:
- 分配路径过长
- 小对象过多
优化案例:Balanced GC(指针碰撞风格对小对象进行分配)
Go编译器优化
-
函数内联:将被调用函数的函数体(callee) 的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
-
Beast Mode:调整函数内联的策略,使更多函数被内联
- 降低函数调用的开销
- 增加了其他优化的机会,如逃逸分析
-
逃逸分析:分析代码中指针的动态作用域,即指针在何处可以被访问
函数内联可以减少逃逸对象的数量
未逃逸的对象可以在栈上分配,对象在栈上的分配和回收很快
参考
- 字节内部课—Go 语言内存管理详解