02-04-原理篇-GC Roots与对象图遍历

GC Roots 与对象图遍历

篇章 :02-原理篇

阅读时间 :约 35 分钟

前置知识:了解堆和栈的基本概念


一、引言

垃圾回收的核心问题是:如何判断一个对象还在被使用? 追踪式 GC(Tracing GC)给出的答案是------从一组被称为 GC Roots 的根引用出发,遍历整个对象引用图(Object Graph),所有可达的对象都是存活的,不可达的对象都是垃圾。

GC Roots 是 GC 的起点,对象图遍历是 GC 的路径,三色标记是 GC 的算法框架。理解这三个概念,就理解了追踪式 GC 的核心工作原理。

本章将深入剖析 GC Roots 的种类、对象图遍历的算法、三色标记的原理,以及并发标记面临的挑战。


二、GC Roots 的种类

GC Roots 是一组特殊的引用,它们是 GC 遍历的起点。一个对象只要从某个 GC Root 可达,就不会被回收。 反之,如果一个对象从所有 GC Root 都不可达,就是垃圾。

2.1 栈变量

方法栈上的局部变量 是最常见的 GC Root。每个线程的调用栈中,所有局部变量引用的对象都是 GC Root。

复制代码
using System;

class StackVariableRoot
{
    static void Main()
    {
        // a 是栈上的局部变量,引用堆上的对象
        // a 就是 GC Root
        var a = new byte[1024];
        
        // b 也是 GC Root
        var b = new byte[2048];
        
        // a 引用 c,c 从 GC Root (a) 可达
        var c = new int[256];
        // 假设有某种方式让 a 引用 c
        
        GC.Collect();
        // a、b、c 都不会被回收(从栈变量可达)
        
        a = null; // 断开 a 的引用
        GC.Collect();
        // a 原来引用的对象被回收
        // c 如果只被 a 引用,也被回收
        // b 仍然存活
    }
}

关键细节:

  • 栈变量在方法返回后自动失效(栈帧弹出),引用的对象变为不可达

  • JIT 编译器会优化掉不使用的局部变量,提前断开引用

  • Debug 模式下不会优化,引用存活时间更长(便于调试)

    // JIT 优化对 GC Root 的影响
    using System;
    using System.Diagnostics;

    class JITOptimizationDemo
    {
    static void Main()
    {
    // Release 模式下,JIT 可能提前断开引用
    var large = new byte[10_000_000]; // 10MB

    复制代码
          // 如果后续不使用 large,JIT 可能在 GC 时认为它已不可达
          // 即使方法还没返回!
          
          GC.Collect();
          // Release: large 可能已被回收
          // Debug: large 不会被回收(调试器保持引用)
          
          // 防止 JIT 优化的技巧
          GC.KeepAlive(large); // 显式保持引用到此处
      }

    }

2.2 静态变量

静态字段 是另一类重要的 GC Root。静态字段的生命周期与类型(或 AppDomain)相同,因此它们引用的对象会长期存活。

复制代码
using System;

class StaticVariableRoot
{
    // 静态字段是 GC Root,引用的对象长期存活
    private static byte[] _cache = new byte[1024 * 1024]; // 1MB
    
    // 静态字典中的值也是从 GC Root 可达的
    private static System.Collections.Generic.Dictionary<string, object> _registry 
        = new System.Collections.Generic.Dictionary<string, object>();
    
    static void Main()
    {
        _registry["key1"] = new byte[512 * 1024]; // 512KB
        _registry["key2"] = new byte[256 * 1024]; // 256KB
        
        GC.Collect();
        // 所有静态引用的对象都不会被回收
        
        // 清除引用才能让对象被回收
        _registry.Clear();
        GC.Collect();
        // 现在 _registry 中的对象可以被回收
        // 但 _cache 仍然存活(静态字段仍引用它)
    }
}

静态变量导致的内存泄漏:

复制代码
// 常见的静态变量内存泄漏模式
using System;
using System.Collections.Generic;

class StaticLeakDemo
{
    // 危险:静态事件订阅会导致订阅者无法被回收
    public static event EventHandler? SomethingHappened;
    
    static void Main()
    {
        var subscriber = new Subscriber();
        SomethingHappened += subscriber.OnEvent;
        
        // 即使 subscriber 变量超出作用域
        // 静态事件仍然引用它 → 内存泄漏!
        
        // 解决方案:显式取消订阅
        SomethingHappened -= subscriber.OnEvent;
    }
}

class Subscriber
{
    public void OnEvent(object? sender, EventArgs e) { }
    
    ~Subscriber()
    {
        Console.WriteLine("Subscriber 被回收");
    }
}

2.3 寄存器

CPU 寄存器 中的引用也是 GC Root。JIT 编译器会将频繁使用的局部变量放入寄存器,这些寄存器中的引用也是 GC 的起点。

复制代码
// 寄存器作为 GC Root 的场景
using System;

class RegisterRoot
{
    static void ProcessLargeData()
    {
        // 紧密循环中的变量可能被放入寄存器
        var buffer = new byte[4096];
        
        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = (byte)(i % 256);
            // buffer 引用可能在寄存器中保持
            // 在循环期间,buffer 是 GC Root
        }
        
        // 循环结束后,buffer 可能从寄存器中移出
        // 如果后续不使用,可能被 GC 回收
    }
}

寄存器作为 GC Root 的处理比较特殊:

  • GC 需要 扫描所有 CPU 寄存器 来找到引用
  • JIT 编译器需要提供 寄存器映射表(Register Map),告诉 GC 哪些寄存器包含引用
  • 这是 .NET GC 与 JIT 编译器紧密协作的体现

2.4 GC Handle

GC Handle 是 .NET 特有的一种 GC Root 类型,用于显式控制对象的生命周期。

复制代码
using System;
using System.Runtime.InteropServices;

class GCHandleDemo
{
    static void Main()
    {
        var obj = new byte[1024];
        
        // Normal Handle:强引用,防止对象被回收
        GCHandle normalHandle = GCHandle.Alloc(obj, GCHandleType.Normal);
        GC.Collect();
        // obj 不会被回收(normalHandle 是 GC Root)
        normalHandle.Free(); // 释放后 obj 可被回收
        
        // Pinned Handle:固定对象地址,防止 GC 移动
        GCHandle pinnedHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
        IntPtr ptr = pinnedHandle.AddrOfPinnedObject();
        Console.WriteLine($"固定地址: {ptr}");
        GC.Collect();
        // obj 不会被移动(pinnedHandle 固定了它)
        pinnedHandle.Free();
        
        // Weak Handle:弱引用,不阻止 GC 回收
        GCHandle weakHandle = GCHandle.Alloc(obj, GCHandleType.Weak);
        GC.Collect();
        Console.WriteLine($"弱引用目标存活: {weakHandle.Target != null}"); // 可能已被回收
        weakHandle.Free();
        
        // WeakTrackResurrection Handle:更短的弱引用
        // 在 Finalizer 执行后也允许回收
        GCHandle weakResHandle = GCHandle.Alloc(obj, GCHandleType.WeakTrackResurrection);
        GC.Collect();
        weakResHandle.Free();
    }
}

GC Handle 类型对比:

Handle 类型 阻止回收 阻止移动 用途
Normal ✅ ❌ 保持对象存活
Pinned ✅ ✅ P/Invoke 传参
Weak ❌ ❌ 弱引用
WeakTrackResurrection ❌ ❌ 更短弱引用

2.5 其他 GC Root 类型

除了上述四种主要类型,.NET 中还有以下 GC Root:

Root 类型 说明
Finalizer 队列 等待执行 Finalizer 的对象
f-reachable 队列 有 Finalizer 但尚未执行的对象
Timer 实例 System.Threading.Timer 的回调引用
Async 状态机 async/await 的状态机字段
委托目标 委托引用的目标对象
互操作引用 COM 互操作中的引用
复制代码
// async/await 状态机作为 GC Root
using System;
using System.Threading.Tasks;

class AsyncStateMachineRoot
{
    static async Task Main()
    {
        var largeBuffer = new byte[10_000_000]; // 10MB
        
        // await 之前,largeBuffer 在栈上
        await Task.Delay(100);
        
        // await 之后,方法返回到线程池
        // largeBuffer 被提升到状态机字段中
        // 状态机是 GC Root,largeBuffer 从状态机可达
        
        Console.WriteLine($"buffer 长度: {largeBuffer.Length}");
        // largeBuffer 在整个 await 期间都不会被回收
    }
}

三、对象图遍历算法

从 GC Roots 出发,GC 需要遍历整个对象引用图,找到所有可达对象。这个过程本质上是图的遍历,主要有两种方式:BFS(广度优先搜索) 和 DFS(深度优先搜索)。

3.1 BFS 遍历

BFS 使用队列(Queue)来管理待访问的节点,按层级顺序遍历对象图:

复制代码
BFS 遍历伪代码:

function BFS_Mark(rootSet):
    queue = new Queue()
    
    // 将所有 GC Root 入队
    for root in rootSet:
        if root != null && !root.marked:
            root.marked = true
            queue.enqueue(root)
    
    while !queue.isEmpty():
        obj = queue.dequeue()
        
        // 遍历对象的所有引用
        for ref in obj.references:
            if ref != null && !ref.marked:
                ref.marked = true
                queue.enqueue(ref)

BFS 的特点:

  • 使用队列(FIFO),按距离 GC Root 的层级顺序遍历

  • 空间复杂度:O(W),W 是对象图的最大宽度

  • 缓存友好:相邻层级的对象在内存中可能更接近

  • .NET 的 GC 使用 BFS,配合一个灰色队列(mark queue)

    // BFS 遍历的 C# 模拟
    using System;
    using System.Collections.Generic;

    class BFSTraversal
    {
    class GraphObject
    {
    public string Name { get; set; }
    public List References { get; } = new();
    public bool Marked { get; set; }
    }

    复制代码
      static void BFSMark(List<GraphObject> roots)
      {
          var queue = new Queue<GraphObject>();
          
          foreach (var root in roots)
          {
              if (root != null && !root.Marked)
              {
                  root.Marked = true;
                  queue.Enqueue(root);
              }
          }
          
          while (queue.Count > 0)
          {
              var obj = queue.Dequeue();
              Console.WriteLine($"标记: {obj.Name}");
              
              foreach (var ref_ in obj.References)
              {
                  if (ref_ != null && !ref_.Marked)
                  {
                      ref_.Marked = true;
                      queue.Enqueue(ref_);
                  }
              }
          }
      }
      
      static void Main()
      {
          // 构建对象图
          var a = new GraphObject { Name = "A" };
          var b = new GraphObject { Name = "B" };
          var c = new GraphObject { Name = "C" };
          var d = new GraphObject { Name = "D" };
          var e = new GraphObject { Name = "E" };
          
          a.References.Add(b);
          a.References.Add(c);
          b.References.Add(d);
          c.References.Add(d);
          d.References.Add(e);
          
          // BFS 标记
          Console.WriteLine("BFS 遍历顺序:");
          BFSMark(new List<GraphObject> { a });
          // 输出: A → B → C → D → E
      }

    }

3.2 DFS 遍历

DFS 使用栈(Stack)来管理待访问的节点,按深度优先顺序遍历对象图:

复制代码
DFS 遍历伪代码(迭代版本):

function DFS_Mark(rootSet):
    stack = new Stack()
    
    for root in rootSet:
        if root != null && !root.marked:
            root.marked = true
            stack.push(root)
    
    while !stack.isEmpty():
        obj = stack.pop()
        
        for ref in obj.references:
            if ref != null && !ref.marked:
                ref.marked = true
                stack.push(ref)

DFS 的特点:

  • 使用栈(LIFO),沿一条路径深入到底再回溯

  • 空间复杂度:O(D),D 是对象图的最大深度

  • 缓存不友好:深路径上的对象在内存中可能不相邻

  • 递归版本有栈溢出风险(对象图很深时)

    // DFS 遍历的 C# 模拟
    using System;
    using System.Collections.Generic;

    class DFSTraversal
    {
    class GraphObject
    {
    public string Name { get; set; }
    public List References { get; } = new();
    public bool Marked { get; set; }
    }

    复制代码
      static void DFSMark(List<GraphObject> roots)
      {
          var stack = new Stack<GraphObject>();
          
          foreach (var root in roots)
          {
              if (root != null && !root.Marked)
              {
                  root.Marked = true;
                  stack.Push(root);
              }
          }
          
          while (stack.Count > 0)
          {
              var obj = stack.Pop();
              Console.WriteLine($"标记: {obj.Name}");
              
              foreach (var ref_ in obj.References)
              {
                  if (ref_ != null && !ref_.Marked)
                  {
                      ref_.Marked = true;
                      stack.Push(ref_);
                  }
              }
          }
      }
      
      static void Main()
      {
          var a = new GraphObject { Name = "A" };
          var b = new GraphObject { Name = "B" };
          var c = new GraphObject { Name = "C" };
          var d = new GraphObject { Name = "D" };
          var e = new GraphObject { Name = "E" };
          
          a.References.Add(b);
          a.References.Add(c);
          b.References.Add(d);
          c.References.Add(d);
          d.References.Add(e);
          
          Console.WriteLine("DFS 遍历顺序:");
          DFSMark(new List<GraphObject> { a });
          // 输出: A → C → D → E → B(取决于引用顺序)
      }

    }

3.3 BFS vs DFS 对比

维度 BFS DFS
数据结构 队列(FIFO) 栈(LIFO)
空间复杂度 O(W)(最大宽度) O(D)(最大深度)
缓存友好性 好(层级遍历) 差(深度遍历)
栈溢出风险 无 递归版本有
.NET 使用 ✅ ❌
Java 使用 ✅ ❌
Boehm GC 使用 ❌ ✅

为什么 .NET 选择 BFS?

  1. 缓存友好:BFS 按层级遍历,同一层级的对象在内存中可能更接近
  2. 无栈溢出风险:BFS 使用堆上的队列,不受调用栈大小限制
  3. 并行友好:BFS 的层级结构更容易并行化(同一层级的节点可以并行处理)

四、三色标记算法

三色标记(Tri-Color Marking)是追踪式 GC 的标准算法框架,由 Edsger Dijkstra 在 1978 年提出。它将对象分为三种颜色,通过颜色转换来跟踪标记进度。

4.1 白色/灰色/黑色

颜色 含义 说明
白色(White) 未被访问 尚未被 GC 访问的对象,GC 结束后仍为白色的对象是垃圾
灰色(Gray) 已访问,未扫描引用 已被 GC 发现但尚未遍历其引用的对象
黑色(Black) 已访问,已扫描引用 已被 GC 完全处理的对象,所有引用都已遍历
复制代码
三色标记不变式(Invariant):

强不变式:不存在从黑色对象到白色对象的引用
  → 黑色对象引用的所有对象至少是灰色或黑色

弱不变式:所有指向白色对象的引用都来自灰色对象
  → 灰色对象是"待扫描"的边界

4.2 标记流程

复制代码
三色标记流程:

初始状态:所有对象都是白色

步骤1:将所有 GC Root 标记为灰色
  [Root1: 灰] [Root2: 灰] [所有其他对象: 白]

步骤2:从灰色对象中取出一个,扫描其引用
  - 将其引用的白色对象标记为灰色
  - 将自身标记为黑色
  
  [Root1: 黑] → [ObjA: 灰] [ObjB: 灰]
  [Root2: 黑] → [ObjC: 灰]

步骤3:重复步骤2,直到没有灰色对象

最终状态:
  [黑色对象] = 存活(可达)
  [白色对象] = 垃圾(不可达)

// 三色标记的 C# 模拟
using System;
using System.Collections.Generic;

class TriColorMarking
{
    enum Color { White, Gray, Black }
    
    class GCObject
    {
        public string Name { get; set; }
        public Color Color { get; set; } = Color.White;
        public List<GCObject> References { get; } = new();
    }
    
    static void Mark(List<GCObject> roots)
    {
        var grayQueue = new Queue<GCObject>();
        
        // 步骤1:GC Root 标记为灰色
        foreach (var root in roots)
        {
            if (root != null && root.Color == Color.White)
            {
                root.Color = Color.Gray;
                grayQueue.Enqueue(root);
            }
        }
        
        // 步骤2-3:处理灰色队列
        while (grayQueue.Count > 0)
        {
            var obj = grayQueue.Dequeue();
            
            // 扫描引用
            foreach (var ref_ in obj.References)
            {
                if (ref_ != null && ref_.Color == Color.White)
                {
                    ref_.Color = Color.Gray; // 白 → 灰
                    grayQueue.Enqueue(ref_);
                }
            }
            
            obj.Color = Color.Black; // 灰 → 黑
            Console.WriteLine($"  {obj.Name}: {Color.Gray} → {Color.Black}");
        }
    }
    
    static void Main()
    {
        // 构建对象图
        var root = new GCObject { Name = "Root" };
        var a = new GCObject { Name = "A" };
        var b = new GCObject { Name = "B" };
        var c = new GCObject { Name = "C" };
        var garbage = new GCObject { Name = "Garbage" };
        
        root.References.Add(a);
        root.References.Add(b);
        a.References.Add(c);
        b.References.Add(c);
        // garbage 不可达
        
        Console.WriteLine("三色标记过程:");
        Mark(new List<GCObject> { root });
        
        Console.WriteLine($"\n存活对象(黑色): Root, A, B, C");
        Console.WriteLine($"垃圾对象(白色): Garbage");
    }
}

4.3 漏标与多标

三色标记在 并发标记(GC 线程与应用线程同时运行)时会面临两个经典问题:

漏标(Missing Mark)

漏标 发生在并发标记期间,应用线程修改了引用关系,导致原本可达的对象被误判为垃圾。

复制代码
漏标场景:

初始状态:
  A(黑) → B(灰) → C(白)

并发操作:
  1. 应用线程:A.field = C  (A 引用 C)
  2. 应用线程:B.field = null (B 不再引用 C)
  3. GC 线程:扫描 B 的引用 → B 没有引用 → B 变黑
  4. GC 线程:A 已经是黑色,不再扫描 A 的引用

结果:C 仍然是白色 → 被误判为垃圾!
      但 A 实际上引用了 C → 悬挂指针!

解决方案:

方案 说明 使用者
增量更新(Incremental Update) 黑色对象新增引用白色对象时,将黑色对象重新标记为灰色 CMS, G1
SATB(Snapshot At The Beginning) 标记开始时记录引用快照,删除引用时保留旧引用 G1, ZGC
写屏障(Write Barrier) 每次引用写入时执行额外操作,维护三色不变式 所有并发 GC
复制代码
// 增量更新的写屏障伪代码
// 当黑色对象新增白色引用时,将黑色对象重新标记为灰色

function IncrementalUpdateWriteBarrier(obj, field, newValue):
    obj.field = newValue
    
    // 如果 obj 是黑色且 newValue 是白色
    if obj.color == Black && newValue.color == White:
        obj.color = Gray  // 重新标记为灰色
        grayQueue.enqueue(obj)
多标(Floating Garbage)

多标 发生在并发标记期间,应用线程删除了对某对象的引用,但该对象已经被标记为黑色。这个对象在本次 GC 中不会被回收(因为已经是黑色),但实际上它已经是垃圾了。

复制代码
多标场景:

初始状态:
  A(黑) → B(黑) → C(黑)

并发操作:
  1. 应用线程:B.field = null (B 不再引用 C)
  2. C 已经是黑色,不会被重新检查

结果:C 被标记为存活,但实际已是垃圾
      → "浮动垃圾"(Floating Garbage)
      → 下一次 GC 才能回收

多标是并发 GC 的固有代价,无法完全避免,但可以接受------浮动垃圾会在下一次 GC 中被回收。


五、并发标记的挑战

5.1 并发标记的目标

现代 GC 力求减少 STW(Stop-The-World)时间,理想情况下标记阶段也应该与应用线程并发执行。但并发标记面临以下挑战:

挑战 说明
漏标问题 应用线程修改引用关系导致漏标
浮动垃圾 已标记的对象变为垃圾,本次无法回收
标记效率 并发标记需要额外的写屏障开销
一致性 需要在某个时间点获取一致的根集合

5.2 .NET 的并发标记策略

.NET 的 Background GC 使用以下策略实现并发标记:

复制代码
.NET Background GC 标记流程:

1. 初始标记(Initial Mark)------ STW(极短)
   - 暂停所有应用线程
   - 记录所有 GC Root
   - 恢复应用线程

2. 并发标记(Concurrent Mark)------ 与应用并发
   - GC 线程从 GC Root 开始遍历
   - 应用线程正常运行
   - 写屏障记录引用变更

3. 重新标记(Remark)------ STW(短)
   - 暂停所有应用线程
   - 处理写屏障记录的变更
   - 修正漏标对象

4. 并发清扫/回收 ------ 与应用并发
   - 回收白色对象
   - 恢复应用线程

// 观察 .NET 并发 GC 行为
using System;
using System.Threading;
using System.Diagnostics;

class ConcurrentGCDemo
{
    static void Main()
    {
        // 启用 Server GC(支持 Background GC)
        // 实际配置在 app.config 或 runtimeconfig.json 中
        Console.WriteLine($"Server GC: {System.Runtime.GCSettings.IsServerGC}");
        Console.WriteLine($"Latency Mode: {System.Runtime.GCSettings.LatencyMode}");
        
        // 并发分配和 GC
        var sw = Stopwatch.StartNew();
        var keepAlive = new System.Collections.Generic.List<byte[]>();
        
        // 模拟并发工作
        for (int i = 0; i < 1_000_000; i++)
        {
            var temp = new byte[64];
            if (i % 100000 == 0)
            {
                keepAlive.Add(new byte[1024 * 1024]); // 1MB,进 LOH
            }
        }
        
        sw.Stop();
        Console.WriteLine($"分配耗时: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Gen0 回收: {GC.CollectionCount(0)}");
        Console.WriteLine($"Gen2 回收: {GC.CollectionCount(2)}");
        Console.WriteLine($"堆大小: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
    }
}

5.3 .NET 的 GC 延迟模式

.NET 提供了不同的延迟模式来控制 GC 的并发程度:

复制代码
using System;
using System.Runtime;

class LatencyModeDemo
{
    static void Main()
    {
        // 查看当前延迟模式
        Console.WriteLine($"当前延迟模式: {GCSettings.LatencyMode}");
        
        // 不同延迟模式的特点:
        // 1. Batch: 最大化吞吐量,STW 时间长
        // 2. Interactive: 平衡吞吐量和延迟(默认)
        // 3. LowLatency: 减少延迟,避免 Gen2 GC
        // 4. SustainedLowLatency: 持续低延迟,几乎不做 Gen2 GC
        // 5. NoGCRegion: 临时禁止 GC(.NET 4.6+)
        
        // 临时进入低延迟区域
        var oldMode = GCSettings.LatencyMode;
        
        // .NET 4.6+: 尝试进入 NoGC 区域
        if (GC.TryStartNoGCRegion(50 * 1024 * 1024)) // 50MB 预算
        {
            try
            {
                // 在此期间不会触发 GC
                DoCriticalWork();
            }
            finally
            {
                GC.EndNoGCRegion();
            }
        }
        
        GCSettings.LatencyMode = oldMode;
    }
    
    static void DoCriticalWork()
    {
        // 游戏帧循环中的关键路径
        // 此处分配不会触发 GC
        for (int i = 0; i < 1000; i++)
        {
            var temp = new byte[1024];
        }
    }
}
延迟模式 Gen0 GC Gen1 GC Gen2 GC 适用场景
Batch ✅ ✅ ✅(阻塞) 批处理、非交互
Interactive ✅ ✅ ✅(并发) 桌面应用(默认)
LowLatency ✅ ✅ ❌ 游戏帧循环
SustainedLowLatency ✅ ✅ ❌ 持续低延迟
NoGCRegion ❌ ❌ ❌ 关键路径

5.4 Unity 中的并发标记

Unity 的 Boehm GC 支持部分并发标记,但与 .NET 的 Background GC 有本质区别:

复制代码
// Unity 中 GC 对游戏帧率的影响
#if UNITY_EDITOR
using UnityEngine;
using System;

public class UnityGCImpact : MonoBehaviour
{
    void Update()
    {
        // 每帧分配少量对象
        // Boehm GC 不分代,每次 GC 都是 Full GC
        var temp = new byte[1024]; // 1KB
        
        // 如果累积分配量达到阈值,触发 GC
        // Boehm GC 的 STW 会导致帧率卡顿
        
        // 检测 GC 发生
        long memBefore = GC.GetTotalMemory(false);
        GC.Collect();
        long memAfter = GC.GetTotalMemory(true);
        
        if (memBefore != memAfter)
        {
            Debug.Log($"GC 回收: {(memBefore - memAfter) / 1024} KB");
        }
    }
}
#endif
特性 .NET Background GC Unity Boehm GC
并发标记 ✅(完全并发) 部分支持
分代 ✅(Gen0/1/2) ❌
写屏障 ✅(Card Table) ❌
STW 时间 短(初始标记+重新标记) 长(Full GC)
浮动垃圾 有(下次回收) 无(STW 期间无并发)
延迟模式 5 种可选 无

六、总结

GC Roots 与对象图遍历是追踪式 GC 的核心机制:

  1. GC Roots 是 GC 遍历的起点,包括栈变量、静态变量、寄存器、GC Handle 等。理解 GC Roots 的种类有助于诊断内存泄漏------任何从 GC Root 可达的对象都不会被回收。

  2. 对象图遍历 有 BFS 和 DFS 两种方式。.NET 使用 BFS(配合灰色队列),因为 BFS 缓存友好且无栈溢出风险。Boehm GC 使用 DFS。

  3. 三色标记 是追踪式 GC 的标准算法框架。白色(未访问)、灰色(已访问未扫描引用)、黑色(已完全处理)。三色不变式保证了标记的正确性。

  4. 漏标与多标 是并发标记的两个经典问题。漏标通过写屏障(增量更新或 SATB)解决,多标产生浮动垃圾(下次 GC 回收)。

  5. 并发标记 是减少 STW 时间的关键。.NET 的 Background GC 通过初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发回收的流程,将 STW 时间压缩到极短。

Unity vs .NET 的关键差异:

  • .NET 使用 BFS + 三色标记 + 写屏障,支持完全并发标记
  • Unity 的 Boehm GC 使用 DFS + 保守式标记,并发支持有限
  • .NET 提供多种延迟模式(LowLatency、NoGCRegion),Unity 无此机制
  • 这就是 .NET 应用 GC 卡顿远少于 Unity 的根本原因

理解 GC Roots 和对象图遍历,不仅能帮助诊断内存泄漏,还能指导编写 GC 友好的代码------减少 GC Root 的数量和深度,缩短 GC 遍历时间。在后续章节中,我们将进入实践篇,探讨如何在 .NET 和 Unity 中优化 GC 性能。

相关推荐
ClickHouseDB1 小时前
ClickHouse 大规模 OpenTelemetry 数据接入架构的可靠性演进
算法
Shaoshing1 小时前
BHU 润石工作室 251019自建赛题解
算法
玩AI的奶茶2 小时前
24GB 显存能跑多大的模型?参数量、精度与显存占用对照表
人工智能·python·算法·ai·aigc·gpu算力·算力租赁
java_nnnn2 小时前
LeetCode刷题双指针(有效三角形的个数+三数之和)Java
算法·leetcode·职场和发展
人工智能培训2 小时前
智能博弈背景下中国AI国防建设的战略价值
大数据·人工智能·算法·重构·生活
flinn_small2 小时前
三分算法:原理、实现与实战应用
数据结构·算法·三分
思茂信息2 小时前
CST软件UV Slice切片后可直接删除指定一侧实体
算法·uv·emc·emi·cst·仿真软件
INGNIGHT3 小时前
944 · 最大子矩阵(前缀和)
数据结构·c++·算法
All for pursuit.3 小时前
【动态规划-6】96.不同的二叉搜索树
数据结构·c++·算法·leetcode·动态规划