share

1. Go 如何避免全堆扫描?

Golang 主要依赖 三色标记清除(Tri-color Mark and Sweep)+ 增量式 GC + 写屏障(Write Barrier) 机制来 尽可能避免完整的全堆扫描

(1)三色标记清除,减少不必要的扫描

Go 采用的是 三色标记清除(Tri-color Mark & Sweep) GC 算法,它的基本思路是:

  • GC 运行时,不是每次都完整扫描整个堆,而是增量式标记存活对象
  • GC 过程与用户代码并发执行,减少 STW(Stop The World)的时间
  • 使用“写屏障(Write Barrier)” 记录对象变更,避免重复扫描

三色标记算法

GC 将所有对象分为三类:

  • 白色(未标记): 尚未被扫描的对象(可能是垃圾)
  • 灰色(已标记但未扫描): 可能存活,仍需递归扫描的对象
  • 黑色(已标记且已扫描): 确定存活,不再访问

如何避免全堆扫描?

Go 不会一次性扫描整个堆,而是通过 “增量标记 + 写屏障” 仅扫描必要对象:

  1. GC 开始时,不会扫描所有对象,而是先从 root set(栈、全局变量、寄存器)出发,标记活跃对象
  2. 增量式扫描灰色对象,逐步标记黑色
  3. 写屏障确保运行期间新创建的对象被正确追踪,避免遗漏

👉 这意味着,长生命周期对象(黑色)在后续 GC 过程中不会重复扫描,从而减少了全堆扫描的可能性!


(2)写屏障(Write Barrier),避免重复扫描

Go 在 并发 GC 过程中使用“写屏障(Write Barrier)” 记录对象的指针变更,使得:

  • 存活对象不会反复被扫描
  • 增量 GC 只扫描“新创建或变更的对象”
  • 避免了频繁全堆扫描

写屏障如何运作?

在 GC 过程中,如果程序修改了某个对象的指针:

  1. 写屏障会记录下被修改的对象
  2. GC 只需要增量式地扫描被修改的对象,而不是重新扫描整个堆
  3. 只要维护一个“变更列表”,GC 只需要增量追踪这些变化的对象

👉 这样,就不需要每次 GC 都扫描整个堆,降低了 GC 的工作量!


(3)对象生命周期优化,减少短命对象的回收成本

Go 通过以下机制减少 GC 压力,使得短命对象的管理更加高效,不需要频繁扫描整个堆

(a)栈上分配(Stack Allocation)

  • 逃逸分析(Escape Analysis):Go 编译器会分析变量作用域:
    • 如果变量不会逃逸到堆上,就分配在栈上
    • 函数返回时,栈上的变量自动销毁,不需要 GC 介入
  • 避免大量短生命周期对象进入堆区,从根本上减少 GC 需要扫描的对象数量

👉 这样,GC 只需要管理堆上的对象,减少了短生命周期对象的干扰

(b)对象池(Object Pool),重用大对象

  • sync.Pool 允许对象复用,避免频繁分配和回收
  • 适用于 临时对象、高频分配对象

👉 这样,GC 需要回收的对象数量减少,从而减少扫描的负担


(4)优化内存分配,避免堆碎片,减少无效扫描

Go 的堆管理采用了 mcache(线程本地缓存)+ mspan(对象分区)+ mcentral(共享缓存)分层内存管理策略,从根本上减少 GC 的工作量。

(a)小对象优先在本地缓存(mcache)分配

  • 线程局部缓存(mcache): 小对象直接在 mcache 分配,避免频繁触发 GC
  • 减少全局锁竞争,提高分配效率

(b)大对象直接分配在大对象区

  • 大对象不走小对象池化策略,直接回收,减少回收代价
  • 降低碎片化问题,减少 GC 需要扫描的区域

👉 这样,Go GC 在进行内存扫描时,不需要遍历整个堆,而是只处理必要的区域


(5)分批 GC,避免一次性回收大量对象

Go 采用 并发 GC + 增量 GC

  • GC 不是一次性回收所有垃圾,而是分阶段、分批完成
  • 每次 GC 只处理部分对象,降低一次性扫描和回收的成本
  • 避免了像 JVM Full GC 那样的“暂停世界”问题

2. 综述:Go 是如何避免全堆扫描的?

优化方式如何减少全堆扫描?
三色标记清除 只扫描必要对象,避免重复扫描已标记存活对象
写屏障(Write Barrier) 只跟踪变更对象,避免重新扫描整个堆
栈上分配 让短生命周期对象不进入堆,减少 GC 负担
对象池(sync.Pool) 避免短命对象频繁分配/回收,降低 GC 负担
分层内存管理(mcache/mspan) 小对象优先在线程局部缓存分配,减少堆分配
大对象优化 直接分配在大对象区,避免碎片化问题
增量 GC 逐步回收,减少一次性回收大量对象的压力

3. 总结

Golang 确实没有“分代 GC”,但它通过多种策略减少了全堆扫描的可能性。
Go 主要依赖

  • 三色标记清除 + 增量 GC,减少 GC 暂停时间
  • 写屏障(Write Barrier),避免重复扫描
  • 逃逸分析(Stack Allocation),减少短命对象进入堆
  • mcache/mspan 管理,减少 GC 需要遍历的对象
  • sync.Pool 缓存对象,减少 GC 负担

🚀 结论:虽然 Go 不是传统的“分代 GC”,但它采用了一系列机制, 有效减少了全堆扫描的情况,提高了 GC 效率!

posted @ 2024-04-17 09:48  不做大哥好多年  阅读(34)  评论(0)    收藏  举报