内存管理
常见的GC模式
- 引用计数(销毁引用,计数器-1,为0,回收)
- 标记清除(mark and sweep) 缺点:每次GC需要暂停所有的运行代码(go1.5之前使用),(go1.5之后使用)变种”三色标记”
- 分代搜集
内存泄漏
- 堆内存泄漏(使用内存后未释放)
- 系统资源泄漏(使用后未关闭句柄)
GC调优
- 减少对象的分配(对象重用)
go程序内存占用大的问题
- 是go的垃圾回收有个触发阈值,这次是4M,gc百分比参数是100,下次触发gc是8M
- 操作系统”拖延症”策略
- 每次申请1M的内存,然后分割成不同大小的span,给对象使用,不使用的5分钟后交还系统
对齐规则
-
结构体的成员变量: 编译器默认对齐长度 和 当前成员变量类型的长度(unsafe.Sizeof) 这2者间取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
-
结构体本身:编译器默认对齐长度 和 结构体的所有成员变量类型中的最大长度 这2者间 取最大数的最小整数倍作为对齐值
-
结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的
如何观察GC
- GODEBUG=gctrace=1
- go tool trace(有图)
- runtime.ReadMemStats
Go并发标记清除的,存在什么难点
- 用户态代码可能并发地更新对象图
什么是根对象
- 全局变量
- 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器
三色标记原理
- 白色:初始状态,表示对象未被访问,可能为垃圾。 - 灰色:对象被标记为可达,但其引用的子对象尚未检查。 - 黑色:对象及其所有子对象均被检查,确定为存活对象。
GC 核心流程
1. 标记准备阶段(STW)
- 暂停所有用户协程(STW),确保一致性。
- 扫描根对象:
遍历栈、全局变量、寄存器等根对象,直接可达的对象标记为灰色,加入待处理队列。 - 启用写屏障:
为后续并发标记阶段捕获对象引用变更。
2. 并发标记阶段
- 恢复用户协程,GC线程与用户程序并发运行。
- 处理灰色队列:
- 取出灰色对象,遍历其引用的子对象。
- 若子对象为白色,则将其标记为灰色并加入队列。
- 当前灰色对象标记为黑色。
- 写屏障监控:
- 用户程序修改对象引用时,触发混合写屏障(Hybrid Write Barrier)。
- 若黑色对象引用白色对象,屏障将白色对象标记为灰色(防止漏标)。
3. 标记终止阶段(STW)
- 再次暂停用户协程,确保所有灰色对象处理完毕。
- 完成剩余标记工作,确认所有存活对象均为黑色。
4. 并发清除阶段
- 回收所有白色对象(垃圾)的内存。
- 用户程序恢复运行。
关键机制
混合写屏障(Hybrid Write Barrier)
- 插入屏障:
黑色对象引用白色对象时,将白色对象标记为灰色。 - 删除屏障:
若被删除引用的对象为灰色或白色,则标记为灰色(Go 1.8+优化后主要依赖插入屏障)。 - 目的:
确保并发标记期间,用户代码修改引用关系不会导致存活对象被误回收。
并发优化
- GC线程与用户协程交替运行(基于Goroutine调度器)。
- 利用CPU多核并行处理标记任务,缩短暂停时间。
有了 GC,为什么还会发生内存泄露
- 预期能被快速释放的内存因被根对象引用而没有得到迅速释放
- 全局变量或缓存无限增长,对象被长期引用。
- 未释放的底层资源
- goroutine 泄漏
- 如果一个 goroutine 尝试向一个没有接收方的无缓冲 channel 发送消息,则该 goroutine 会被永久的休眠,整个 goroutine 及其执行栈都得不到释放
触发 GC 的时机是什么?
- 主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。
- 被动触发,分为两种方式:
- 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
- 使用步调(Pacing)算法,其核心思想是控制内存增长的比例。
GC优化
- 控制内存分配的速度,限制 goroutine 的数量,从而提高赋值器对 CPU 的利用率。
- 减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如提前分配足够的内存来降低多余的拷贝。
- 需要时,增大 GOGC 的值,降低 GC 的运行频率。