开篇
本文为博主学习java跟go的gc内容时写下文章, 目的是为了对gc作出总结, 文章中有不对的地方请指正。
Go的GC
三色标记法
如何判断go堆中的对象, 是否需要清理呢? 在go中使用三色标记法来记录存活对象。
大概流程
一开始所有对象都为白色, 首先从根对象 (Goroutine栈、全局变量、寄存器指针)出发, 将根对象标记成黑色, 遍历一遍跟根对象有联系的点,将其标成灰色, 完成上述流程, 继续从灰对象作为根对象开始标记,以此类推, 最终所有有联系的对象都被标成黑色,剩下白色的点就是需要清理的对象了。
标记清理算法
在go gc中,清理无用对象是通过标记-清理算法 来清理, 前面的通过三色标记法判断需要清理的对象后就将这些对象直接清理, 简单高效, 执行起来容易, 但是, 这时候就有人问了, 标记-清理算法不是会产生内存碎片吗? go设计者设计的时候没考虑到吗?
确实, 标记-清理算法会产生很多的内存碎片, 但go的内存模型可以解决这一问题。
内部碎片:通过大小类(Size Classes)标准化分配
- 机制:Go 预定义 67 个固定大小类(8B、16B、32B…32KB)。分配对象时,内存向上对齐到最接近的大小类(如 13B → 16B)。
- 效果:
- 内部碎片被控制在固定范围内(例如 13B 分配 16B,产生 3B 内部碎片)。
- 同一大小类的对象集中存储,内存块规格统一,分配器无需为不同大小的对象预留复杂空间。
Java G1 GC
在 JDK 1.9 后默认的GC也就是G1, 一种并发回收器, 作用于整个堆。也是使用三色标记法标记可回收对象。
物理分代的革新:Region 分区机制
- Region 动态角色:
- G1 将堆划分为 固定大小的 Region(默认 1MB–32MB,通过
-XX:G1HeapRegionSize
调整)。 - 每个 Region 可动态扮演以下角色:
- Eden Region:存储新创建对象(年轻代)。
- Survivor Region:存储 Minor GC 后存活的对象(年轻代)。
- Old Region:存储长期存活对象(老年代)。
- Humongous Region:存储大小超过 Region 50% 的大对象(连续多个 Region 组成)。
- G1 将堆划分为 固定大小的 Region(默认 1MB–32MB,通过
- 逻辑分代,物理非连续:
- 新生代和老年代不再是连续内存区域,而是由分散的 Region 逻辑组成,避免内存碎片。
- 年轻代占比动态调整(默认 ≤50%),老年代随对象晋升自动扩展。
年轻代回收:标记-复制算法的优化
1. 回收流程(Minor GC)
- 触发条件:Eden Region 满时触发。
- 核心步骤:
- STW 初始标记:暂停线程,扫描 GC Roots 直接可达对象(与年轻代回收绑定)。
- 并发标记:与应用线程并行标记存活对象(通过写屏障追踪引用变化)。
- 复制存活对象:
- 存活对象从 Eden/Survivor Region 复制到新的 Survivor Region(比例为 8:1:1)。
- 年龄达阈值(默认 15)的对象晋升至 Old Region。
- 无碎片问题:复制算法确保回收后 Survivor Region 空间连续。
2. 跨代引用优化:Remembered Set (RSet)
- RSet 的作用:
- 每个 Region 维护一个 RSet,记录哪些其他 Region 的对象引用了本 Region(如老年代 → 年轻代引用)。
- 避免 Minor GC 时扫描全堆,仅需检查 RSet 中的引用卡片(Card)。
- 维护机制:
- 写屏障(Write Barrier)捕获引用更新,将对应卡表(Card Table)标记为 “Dirty”。
- 后台线程(Concurrent Refinement Threads)异步更新 RSet。
🧓 老年代回收:混合标记-整理算法
1. 混合回收(Mixed GC)
- 触发条件:老年代占比超过阈值(
-XX:InitiatingHeapOccupancyPercent=45%
)。 - 回收范围:
- 所有年轻代 Region + 部分老年代 Region(优先选择垃圾占比高的 Region,即 “Garbage-First”)。
- 核心阶段:
- 并发标记周期(Concurrent Marking Cycle):
- 初始标记(STW):标记 GC Roots 直接关联对象。
- 并发标记:遍历对象图,SATB 算法保证一致性(解决漏标问题)。
- 最终标记(STW):处理 SATB 队列中的引用变更。
- 转移存活对象:
- 将存活对象从待回收 Region 复制到空闲 Region(复制即整理,消除碎片)。
- 并发标记周期(Concurrent Marking Cycle):
2. 大对象(Humongous)优化
- 直接分配至 Humongous Region:
- 避免大对象进入老年代引发碎片(传统回收器需 Full GC 整理)。
- 专属回收策略:
- 在并发标记的清理阶段或 Full GC 时回收。
⚡ Go GC vs Java G1:核心差异与场景适配
维度 | Go GC | Java G1 | 胜负手 |
---|---|---|---|
STW时间 | 微秒级(常态<100μs)**** | 毫秒级(G1优化后1-10ms) | Go胜出(实时系统首选) |
内存碎片 | 大小类+内存池复用(无需整理) | 复制转移实现零碎片 | 平手(方案不同均有效) |
高并发响应 | Goroutine轻量(2KB栈)支持百万级并发 | 线程模型(1MB/线程)易成瓶颈 | Go碾压级优势 |
吞吐量 | 中高(延迟优先) | 极高(TB级堆稳定运行) | Java胜出(计算密集型场景) |
调优复杂度 | 仅GOGC 参数控制回收频率 | 20+参数协同调优(如Region大小) | Go降低90%运维成本 |
场景化指南
- ✅ Go统治区:
- 实时通信(WebSocket消息低延迟)、边缘设备(内存<4GB)、微服务网关(Envoy改造QPS↑30%)。
- ✅ G1主战场:
- 大数据计算(Spark堆内存>32GB)、企业级单体应用(SAP系统日均Full GC降至0次)。
💎 总结:设计哲学的终极对决
Go的极简主义:
- 用逃逸分析绕过分代难题,三色标记+混合写屏障实现“无感GC”
- 大小类内存池根治碎片,以零配置征服云原生战场
G1的精密工程:
- Region动态分代+SATB快照,以可预测停顿驯服TB级堆内存
- RSet跨代引用追踪,用调优复杂度换取工业级可靠性
没有胜负,只有场景适配:
- 追求亚毫秒延迟、快速扩容?选Go
- 需要处理TB级数据、已有JVM生态?选Java