所谓垃圾,就是不再需要的内存块;这些垃圾如果不清理就没有办法再次被分配使用,所以需要进行垃圾回收。
垃圾回收算法
引用计数
对于每一个对象,维护一个引用计数,当引用该对象的对象被销毁时,引用计数减一,当引用计数为0则回收该对象
优点:
- 对象可以很快地被回收,不会出现内存耗尽或达到某个阈值时再回收
缺点:
- 不能很好的处理循环引用
- 每次对象引用关系发生变化时都需要更新引用计数,可能会增加运行时的开销。
- 引用计数需要保证并发安全,增减计数可能需要加锁,影响并发性能。
分代收集
按照对象生命周期的长短划分不同的代空间,生命周期长的放入老年代,生命周期短的放入新生代;新生代中的对象生命周期短,采用复制算法,老年代中的对象生命周期长,采用标记-清除算法,这样可以根据对象的特性优化垃圾回收效率。
优点:
- 根据对象的生命周期分为不同代,每代使用不同的回收策略,可以更高效地回收内存。
- 大多数对象的生命周期很短,新生代采用复制算法可以快速回收,而老年代则可以利用标记-清除等算法进行优化。
- 对新生代进行频繁且快速的回收,可以减少全局的停顿时间。
缺点:
- 需要维护多代之间的引用关系和回收策略,实现起来较为复杂。
- 需要额外的空间来存储和管理多代对象,尤其是老年代对象可能占用较大的内存空间。
标记清除
从根变量开始遍历所有引用的对象,引用的对象标记为”被引用“,没有被标记的对象会被回收
优点:
- 解决了引用计数的缺点
- 能够正确处理循环引用的情况,因为它通过可达性分析来判断对象是否可回收。
缺点:
- 在标记阶段,可能会导致一段时间的停顿,影响程序的响应速度。
- 整个垃圾回收过程中,可能需要停止整个程序的运行,直到垃圾回收完成,这对实时性要求高的应用有影响。
- 实现相对复杂,需要考虑如何高效地遍历对象,特别是在大型堆内存中。
V1.3版本之前,标记清除法:
- 暂停程序,找出所有可达对象和不可达对象,讲可达对象做上标记,清除所有没有标记的对象,最后继续运行程序,循环这个过程直到程序生命周期结束
V1.5 三色标记法:
-
新创建的对象默认标记为白色,每次GC开始时从根节点开始遍历,将遍历到的白色对象放入灰色集合中,在遍历灰色集合,将灰色集合引用的对象放入灰色集合中,将该灰色对象放入黑色集合,重复该过程直到灰色集合为空,清除所有白色对象
-
强弱三色不变性:
- 强三色不变性:黑色对象不会指向白色对象
- 弱三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径
-
屏障机制:
- 插入屏障:在A对象引用B对象的时候,将B对象标记为灰色
- 删除屏障:被删除的对象,如果自身为灰色或白色,那么被标记为灰色
V1.8混合写屏障机制:
- GC开始,将栈上的对象全部可达扫描并标记为黑色,GC期间任何在栈上创建的新对象均为黑色;被删除的对象标记为灰色,被添加的对象标记为灰色
GC回收触发时机
内存分配达到阀值触发
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即当内存扩大一倍时启动GC。
GC回收触发时机
内存分配达到阀值触发
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即当内存扩大一倍时启动GC。
定期触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9
手动触发
程序中可以通过 runtime.GC()来手动触发GC