GC的算法
在C# 中,垃圾回收器(GC, Garbage Collector)是一个自动内存管理系统。C# 的 GC 主要采用标记-清除(Mark-Sweep)和分代回收(Generational Collection)的方式来高效管理和回收不再使用的内存。下面分别介绍这两种机制。
1. 标记-清除(Mark-Sweep)
工作原理:
标记-清除是垃圾回收的核心算法之一,主要分为两个阶段:标记阶段和清除阶段。
-
标记阶段(Mark phase):
- 从**根对象(Root objects)**出发(如全局变量、线程栈上的局部变量和静态变量),GC 遍历所有直接或间接引用的对象,将这些对象标记为“存活”。
- 这个过程类似于图的遍历:GC 通过深度优先或广度优先的方式遍历对象引用图,标记所有可达的对象。
-
清除阶段(Sweep phase):
- 完成标记后,所有未被标记的对象都被认为是垃圾(即不再需要的内存)。
- GC 会遍历堆中所有对象,并清除未被标记的对象,回收这些对象所占用的内存。
优点:
- 简单有效:算法简单,能够很好地处理对象的引用和生命周期管理。
缺点:
- 碎片化问题:由于标记-清除只是简单地释放内存,回收的内存位置可能是不连续的,导致堆内存碎片化,从而影响后续内存分配效率。
- 暂停时间较长:GC 必须暂停程序执行,等待标记和清除操作完成,可能导致短暂的程序“卡顿”。
2. 分代回收(Generational Collection)
工作原理:
分代回收是基于对象生命周期的观察进行优化的一种垃圾回收策略。C# 的垃圾回收器将堆分为三代,每代对象的回收策略有所不同:
-
第0代(Gen 0):
- 短命对象:新分配的对象会首先进入第0代。这一代通常包含生命周期较短的对象,诸如局部变量等。
- 快速回收:由于大多数对象很快就会变得不可达,第0代会被频繁回收。GC 在回收第0代时,只检查第0代中的对象是否存活,回收其余内存。
-
第1代(Gen 1):
- 中期存活对象:如果第0代中的对象在一次GC回收后依然存活,它们会被提升到第1代。
- 不太频繁回收:第1代的回收频率比第0代低,通常用于回收生命周期较长但最终会被释放的对象。
-
第2代(Gen 2):
- 长期存活对象:如果第1代中的对象依然存活,它们会被提升到第2代。第2代主要存放生命周期较长的对象,比如全局对象、缓存等。
- 较少回收:第2代的回收最为不频繁,因为这些对象可能长期存在。回收第2代通常是内存消耗较大时才触发的(称为完全回收)。
为什么分代回收有效:
- 对象生命周期的特点:大部分对象的生命周期较短,因此第0代频繁回收可以快速释放大量内存。同时,由于较长生命周期的对象数量较少且不经常变化,减少对第1代和第2代的回收频率可以提升性能。
分代回收的策略:
- 代内的局部性:GC 可以只在特定代进行回收,减少不必要的工作,避免完全扫描所有对象,从而提高效率。
- 提升机制:当一个对象在一次回收后依然存活,GC 会认为它可能长期存活,因此将其提升到更高的一代。提升后的对象在下一代会经历较少的回收频率。
优点:
- 高效的内存管理:分代回收利用了对象生命周期的局部性,能够在不频繁扫描整个堆的情况下,快速回收短生命周期对象,降低GC开销。
- 减少暂停时间:因为可以针对特定代进行垃圾回收,避免对整个内存进行全面扫描,减少了程序因垃圾回收暂停的时间。
缺点:
- 提升可能带来开销:对象从第0代到第1代、再到第2代的提升会带来一些额外的开销,尤其是当对象需要频繁提升时。
- 第2代回收代价高:第2代对象通常是大对象,回收时可能涉及复杂的操作,并且当第2代触发回收时,会导致一次较长的暂停。
3. 标记-压缩 (Mark-Compact)
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。
不过在 标记阶段后它将所有活动对象紧密的排在堆的一侧(压缩),消除了内存碎片,然后清理边界以外的垃圾,从而 解决了碎片化的问题。
不过压缩是需要花费计算成本的。
缺点
- 压缩过程的开销,需要多次搜索堆:
- 标记后需要定位各个活动对象的新内存地址,然后再移动对象,总共搜索了3次堆。
4. 引用计数法(Reference Count)
引用技术算法是唯一一种不用用到根集概念的GC算法。其基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。
缺点:
- 最大的缺点,就是无法释放循环引用的对象。
- 必须在引用发生增减时对引用计数做出正确的增减,而如果漏掉了某个增减的话,就会引发很难找到原因的内存错误。
- 引用计数管理并不适合并行处理。
C# 的垃圾回收器通过结合这两种机制,能够在保持良好性能的同时,自动管理内存的分配与回收,极大地简化了开发者的工作。