02-06-原理篇-增量式GC与并发GC

增量式 GC 与并发 GC

篇章 :02-原理篇

阅读时间 :约 40 分钟

前置知识:了解 STW 暂停


一、引言

在游戏和实时交互应用中,GC暂停(Stop-The-World, STW)是最令人头疼的性能问题之一。一次Full GC可能暂停几十甚至上百毫秒,导致明显的卡顿。为了减少暂停时间,GC设计者们发展出了三种核心策略:增量式GC (Incremental GC)、并发式GC (Concurrent GC)和并行式GC(Parallel GC)。

这三种策略的目标相同------减少STW暂停时间------但实现方式截然不同。理解它们的原理、权衡和适用场景,是优化GC性能的关键。Unity的Boehm GC、Mono的SGen GC、.NET的GC各自采用了不同的策略组合,这也直接决定了它们在不同场景下的表现差异。


二、Stop-The-World 的问题

2.1 STW的必要性

传统的标记-清扫GC在工作时需要暂停所有应用线程,这是因为:

  1. 一致性:GC需要看到堆的一致快照。如果应用线程在GC标记过程中修改了对象图,标记结果就会不正确。

  2. 安全性:GC移动对象(压缩阶段)时,应用线程不能访问正在移动的对象,否则会读到无效地址。

  3. 简单性:STW使得GC的实现大大简化,不需要处理并发访问的复杂情况。

    应用线程: ████░░░░░░░░░░████████████
    GC线程: ████
    ↑
    STW暂停

2.2 STW的代价

复制代码
// STW对游戏的影响
// 假设60FPS游戏,每帧16.67ms
// 一次Full GC暂停50ms → 丢失3帧 → 明显卡顿

void Update() {
    // 每帧分配一些临时对象
    var temp = new List<int>(100);
    for (int i = 0; i < 100; i++) {
        temp.Add(i);
    }
    // temp在下一次GC时被回收
    
    // 如果GC在帧中间触发 → STW暂停 → 帧时间超标
}

2.3 STW暂停的组成

STW暂停不是单一的时间块,而是由多个阶段组成:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    GC暂停时间                            │
├──────────┬──────────┬──────────┬──────────┬─────────────┤
│ 初始标记  │  根扫描   │  标记     │  清扫     │   压缩      │
│ (STW)    │  (STW)   │  (STW)   │  (STW)   │   (STW)     │
└──────────┴──────────┴──────────┴──────────┴─────────────┘

每个阶段都贡献了暂停时间。增量式和并发GC的核心思想就是将其中某些阶段从STW中移出,让它们与应用线程并发执行。


三、增量式 GC(Incremental GC)

3.1 时间分片

增量式GC的核心思想是将一次长的GC操作拆分成多次短的GC操作,每次只执行一小部分工作,然后让应用线程运行一段时间,如此交替进行。

复制代码
传统GC:
  应用: ████░░░░░░░░░░░░████████
  GC:        ████████████
             └── 一次长暂停 ──┘

增量式GC:
  应用: ████░░██░░██░░██░░████████
  GC:        ██  ██  ██  ██
             └── 多次短暂停 ──┘

// 增量式GC的概念模型
class IncrementalGC {
    private int _totalWork;
    private int _completedWork;
    private int _workPerStep;
    
    // 每次只做一小部分工作
    public void DoGCStep() {
        int work = Math.Min(_workPerStep, _totalWork - _completedWork);
        
        // 执行一小部分标记/清扫工作
        for (int i = 0; i < work; i++) {
            MarkOneObject();
            _completedWork++;
        }
        
        if (_completedWork >= _totalWork) {
            FinishGC();
        }
    }
    
    // 应用线程和GC交替执行
    public void RunLoop() {
        while (true) {
            // 应用运行一段时间
            RunApplication();
            
            // GC做一小步
            DoGCStep();
        }
    }
}

3.2 写屏障

增量式GC面临一个核心问题:在GC暂停期间,应用线程修改了对象引用关系,导致GC的标记结果可能不正确。

复制代码
问题场景:
  1. GC标记对象A为"已标记"
  2. GC暂停,应用线程运行
  3. 应用线程将A.field从B改为C
  4. B现在不可达,但GC已经标记过A了
  5. GC恢复后不会重新检查A → B被遗漏 → 错误回收!

为了解决这个问题,增量式GC引入了写屏障(Write Barrier)。写屏障是一段在每次引用写入时执行的代码,记录被修改的引用,确保GC不会遗漏。

复制代码
// 写屏障的概念模型
class WriteBarrier {
    // 记录被修改的对象
    private HashSet<object> _modifiedObjects = new();
    
    // 每次引用写入时调用
    public void WriteBarrierWrite(object obj, string field, object value) {
        // 记录被修改的对象
        _modifiedObjects.Add(obj);
        
        // 执行实际写入
        SetField(obj, field, value);
    }
}

// 在C#中,写屏障由运行时自动插入
// 开发者不需要手动实现
// 但理解写屏障的开销很重要:
// - 每次引用写入都有额外开销
// - 可能影响缓存性能
// - 增加了内存开销(记录表)

3.3 写屏障的类型

复制代码
┌─────────────────────────────────────────────────────────┐
│                  写屏障类型                              │
├─────────────────────┬───────────────────────────────────┤
│  精确写屏障          │  记录被写入的具体字段位置           │
│ (Precise)           │  开销大但精确                      │
├─────────────────────┼───────────────────────────────────┤
│  对象写屏障          │  记录被修改的对象                  │
│ (Object)           │  开销中等,需要重新扫描对象         │
├─────────────────────┼───────────────────────────────────┤
│  卡表写屏障          │  将堆分成卡片,记录被修改的卡片     │
│ (Card Table)       │  开销小,最常用                     │
└─────────────────────┴───────────────────────────────────┘

3.4 卡表(Card Table)详解

卡表是.NET GC实际使用的写屏障实现方式。它将堆划分为固定大小的"卡片"(通常512字节或1KB),用一个字节数组记录哪些卡片被修改过。

复制代码
堆内存:
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ Card0 │ Card1 │ Card2 │ Card3 │ Card4 │ Card5 │ Card6 │ Card7 │
│ 512B  │ 512B  │ 512B  │ 512B  │ 512B  │ 512B  │ 512B  │ 512B  │
└───┬──┴───┬──┴──────┴───┬──┴──────┴───┬──┴──────┴──────┴──────┘
    │      │            │              │
    ↓      ↓            ↓              ↓
卡表:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 0  │ 1  │ 0  │ 1  │ 0  │ 1  │ 0  │ 0  │
└────┴────┴────┴────┴────┴────┴────┴────┘
     ↑         ↑         ↑
   被修改    被修改    被修改

// 卡表的工作流程
// 1. 应用线程写入引用时:
//    - 计算写入地址对应的卡片索引
//    - 将卡表对应位置标记为"脏"(dirty)
//    - 执行实际写入

// 2. GC恢复时:
//    - 扫描卡表中所有"脏"的卡片
//    - 重新标记这些卡片中的对象
//    - 清除卡表标记

// 伪代码
void WriteBarrier(ref object field, object value) {
    // 计算卡片索引
    long cardIndex = (long)&field / CARD_SIZE;
    
    // 标记卡片为脏
    cardTable[cardIndex] = 1;
    
    // 执行写入
    field = value;
}

void RescanDirtyCards() {
    for (int i = 0; i < cardTable.Length; i++) {
        if (cardTable[i] == 1) {
            // 重新扫描这个卡片中的所有对象
            ScanCard(i);
            cardTable[i] = 0; // 清除标记
        }
    }
}

3.5 安全点

增量式GC需要在合适的时机暂停应用线程。不是任意时刻都可以暂停------必须在**安全点(Safepoint)**处暂停。安全点是指应用线程处于可以安全暂停的状态,此时所有引用都在已知位置。

复制代码
// 安全点的概念
// 
// 不是安全点:
//   - 正在计算对象引用的中间状态
//   - 正在解引用指针
//   - 正在执行JNI调用
//
// 是安全点:
//   - 方法入口/出口
//   - 循环回边
//   - 方法调用前
//   - 分配新对象前

// JIT编译器在代码中插入安全点检查
// 伪代码:
void Method() {
    while (condition) {
        // 循环回边 → 安全点
        CheckSafepoint();
        
        DoWork();
    }
}

void CheckSafepoint() {
    if (gcRequested) {
        // 暂停自己,等待GC完成
        WaitForGC();
    }
}

3.6 增量式GC的优缺点

优点 缺点
暂停时间短且可预测 总GC时间更长(写屏障开销)
适合实时应用 实现复杂
帧率更稳定 需要额外的内存记录修改
可控制每次增量的大小 可能需要多次暂停才能完成一次GC

四、并发式 GC(Concurrent GC)

4.1 并发标记

并发式GC更进一步------让GC的大部分工作与应用线程同时运行,而不是交替运行。只有少数关键阶段需要STW。

复制代码
并发GC的时间线:

应用线程: ████████████████████████████████
GC线程:          ████░░░░░░░░░░░░░░░███
                 ↑  ↑              ↑  ↑
                 │  │              │  │
              初始标记  并发标记    重新标记  并发清扫
              (STW)   (并发)      (STW)    (并发)

// 并发GC的阶段
class ConcurrentGC {
    // 阶段1: 初始标记 (STW, 很短)
    // - 标记所有GC根直接引用的对象
    // - 暂停时间很短,因为只扫描根
    public void InitialMark() {
        StopTheWorld();
        foreach (var root in GCRoots) {
            MarkObject(root);
        }
        ResumeApplication();
    }
    
    // 阶段2: 并发标记 (与应用并发运行)
    // - 从初始标记的对象开始,遍历整个对象图
    // - 应用线程可以继续运行
    // - 使用写屏障记录修改
    public void ConcurrentMark() {
        while (!markQueue.IsEmpty) {
            var obj = markQueue.Dequeue();
            foreach (var child in GetReferences(obj)) {
                if (!IsMarked(child)) {
                    MarkObject(child);
                    markQueue.Enqueue(child);
                }
            }
        }
    }
    
    // 阶段3: 重新标记 (STW, 很短)
    // - 处理并发标记期间应用线程修改的引用
    // - 扫描写屏障记录的修改
    // - 暂停时间短,因为只处理增量
    public void Remark() {
        StopTheWorld();
        foreach (var modified in GetModifiedObjects()) {
            ReScan(modified);
        }
        ResumeApplication();
    }
    
    // 阶段4: 并发清扫 (与应用并发运行)
    // - 清扫未标记的对象
    // - 回收内存
    // - 应用线程可以继续运行
    public void ConcurrentSweep() {
        foreach (var block in heap) {
            if (!IsMarked(block)) {
                Free(block);
            }
        }
    }
}

4.2 并发标记的挑战

并发标记期间,应用线程在修改对象图,这带来了几个挑战:

复制代码
// 挑战1: 三色标记的不变性
// 
// 三色标记法:
// - 白色: 未访问
// - 灰色: 已访问,但子节点未全部访问
// - 黑色: 已访问,子节点全部访问
//
// 不变性: 黑色对象不能指向白色对象
// (如果黑色→白色,白色会被遗漏)
//
// 并发标记时,应用线程可能:
// 1. 黑色对象 → 新引用 → 白色对象 → 违反不变性!
// 2. 解决: 写屏障在黑色→白色时,将白色变灰

// 挑战2: 浮动垃圾
// 并发标记期间产生的垃圾不会被本次GC回收
// 因为它们可能已经被标记了
// → 需要等下一次GC

// 挑战3: 标记速度 vs 分配速度
// 如果应用分配速度 > GC标记速度
// → GC永远追不上 → 内存持续增长
// → 需要退化为STW

class ConcurrentMarkChallenge {
    // 应用线程修改引用
    void ApplicationCode() {
        var a = new object(); // 白色
        blackObject.field = a; // 黑色→白色
        
        // 写屏障: 将a变灰
        // 确保a会被标记
        WriteBarrier(blackObject, a);
    }
}

4.3 并发清扫

清扫阶段也可以并发执行。由于清扫不移动对象,应用线程可以安全地继续运行:

复制代码
// 并发清扫的优势:
// 1. 清扫不需要暂停应用线程
// 2. 清扫只是遍历堆并释放未标记的对象
// 3. 应用线程可以继续分配新对象
//
// 但需要注意:
// 1. 清扫期间不能压缩堆
// 2. 可能产生碎片
// 3. 新分配的对象需要特殊处理(不能被误清扫)

// .NET的并发清扫
// .NET Server GC使用后台GC (Background GC)
// - Gen0和Gen1仍然STW
// - Gen2的标记和清扫可以并发
// - Gen2的压缩仍然需要STW

4.4 并发GC的暂停时间分析

复制代码
并发GC暂停时间组成:

┌──────────┬──────────────────────────────────┬──────────┐
│ 初始标记  │        并发标记 + 重新标记         │ 并发清扫  │
│ (STW)    │   (并发, 仅重新标记STW)           │ (并发)   │
│ ~1-5ms   │   重新标记: ~1-5ms               │ ~0ms     │
└──────────┴──────────────────────────────────┴──────────┘

总STW暂停: 初始标记 + 重新标记 ≈ 2-10ms
(相比传统Full GC的50-200ms大幅减少)

五、并行式GC(Parallel GC)

5.1 并行 vs 并发

并行GC和并发GC容易混淆,但它们是不同的概念:

特性 并发GC (Concurrent) 并行GC (Parallel)
与应用的关系 GC与应用同时运行 GC暂停应用,但用多线程GC
目标 减少暂停次数 减少暂停持续时间
线程模型 GC线程 + 应用线程 多个GC线程,应用暂停
复杂度 高(需要写屏障) 中(多线程协调)
适用场景 低延迟应用 高吞吐应用

5.2 并行标记

复制代码
// 并行标记: 多个GC线程同时标记
class ParallelMark {
    private ConcurrentQueue<object> _markQueue = new();
    private int _activeWorkers;
    
    public void ParallelMarkPhase() {
        // 多个GC线程同时工作
        int numThreads = Environment.ProcessorCount;
        var tasks = new Task[numThreads];
        
        for (int i = 0; i < numThreads; i++) {
            tasks[i] = Task.Run(() => MarkWorker());
        }
        
        Task.WaitAll(tasks);
    }
    
    private void MarkWorker() {
        while (_markQueue.TryDequeue(out var obj)) {
            if (!IsMarked(obj)) {
                MarkObject(obj);
                
                // 将子节点加入队列
                foreach (var child in GetReferences(obj)) {
                    _markQueue.Enqueue(child);
                }
            }
        }
    }
}

5.3 并行清扫与压缩

复制代码
// 并行清扫: 将堆分成多个区域,每个线程负责一个区域
class ParallelSweep {
    public void ParallelSweepPhase() {
        var regions = DivideHeapIntoRegions(Environment.ProcessorCount);
        
        Parallel.ForEach(regions, region => {
            foreach (var block in region) {
                if (!IsMarked(block)) {
                    Free(block);
                }
            }
        });
    }
    
    // 并行压缩更复杂
    // 需要计算每个对象的移动目标地址
    // 然后并行移动
    public void ParallelCompactPhase() {
        // 1. 计算移动计划(单线程或并行)
        // 2. 并行移动对象
        // 3. 并行更新引用
    }
}

5.4 .NET的并行GC

复制代码
// .NET有两种GC模式:
// 
// 1. Workstation GC (默认)
//    - 单线程GC
//    - 适合客户端应用
//    - 暂停时间可能较长
//
// 2. Server GC
//    - 多线程并行GC
//    - 每个CPU一个GC线程和独立堆
//    - 适合服务器应用
//    - 暂停时间短,吞吐量高
//    - 内存占用更大

// 配置Server GC (app.config)
// <configuration>
//   <runtime>
//     <gcServer enabled="true"/>
//   </runtime>
// </configuration>

// 代码中检查
Console.WriteLine($"GC Server: {GCSettings.IsServerGC}");
Console.WriteLine($"GC Latency Mode: {GCSettings.LatencyMode}");

六、三者的对比

6.1 核心对比表

特性 增量式GC 并发式GC 并行式GC
核心思想 拆分GC工作 GC与应用同时运行 多线程加速GC
STW次数 多次短暂停 2次短暂停 1次(但用多线程加速)
STW总时间 中等 最短 较短
总GC时间 最长(写屏障开销) 中等 最短
吞吐量 最低 中等 最高
延迟 低 最低 中等
实现复杂度 高 最高 中等
内存开销 中等 较高 较低
适用场景 实时系统 低延迟服务 高吞吐服务

6.2 暂停时间对比

复制代码
场景: 堆大小 1GB, 4核CPU

传统STW GC:
  暂停: ████████████████████████ (200ms)

增量式GC:
  暂停: ██ ██ ██ ██ ██ ██ ██ ██ (8×5ms = 40ms总暂停)
  应用: ░░██░░██░░██░░██░░██░░██░░██░░██░░

并发式GC:
  暂停: ████░░░░░░░░░░░░░░░░██ (2×5ms = 10ms总暂停)
  并发:     ██████████████████

并行式GC:
  暂停: ██████████ (50ms, 4线程并行)
  (单线程需要200ms, 4线程约50ms)

6.3 吞吐量对比

复制代码
吞吐量(单位时间内的有效计算时间):

传统STW GC:  ████████████░░░░░░░░  60%
增量式GC:    ████████░░░░░░░░░░░░  40% (写屏障开销大)
并发式GC:    ███████████░░░░░░░░  55% (中等开销)
并行式GC:    ████████████████░░░  80% (高效利用多核)

七、实际 GC 中的混合使用

7.1 .NET GC的混合策略

.NET GC不是单一的策略,而是根据代和GC模式混合使用多种策略:

复制代码
// .NET GC的混合策略
// 
// Gen0 GC (Minor GC):
//   - STW + 并行标记/清扫
//   - 暂停时间短(堆小)
//   - 频率高
//
// Gen1 GC:
//   - STW + 并行标记/清扫
//   - 暂停时间中等
//   - 频率中
//
// Gen2 GC (Major/Full GC):
//   - 并发标记(Background GC)
//   - STW初始标记 + STW重新标记
//   - 并发清扫
//   - 压缩时STW(如果需要)
//
// 配置:
// GCSettings.LatencyMode = GCLatencyMode.Interactive; // 默认
// GCSettings.LatencyMode = GCLatencyMode.LowLatency;  // 低延迟
// GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; // 持续低延迟
// GCSettings.LatencyMode = GCLatencyMode.Batch;       // 批处理(高吞吐)

// 低延迟模式
// - 限制Gen2 GC的触发
// - 适合游戏和实时应用
void ConfigureLowLatency() {
    GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
    // Gen0和Gen1仍然正常GC
    // Gen2 GC被延迟(除非内存压力极大)
}

7.2 Background GC

复制代码
// .NET 4.0引入Background GC
// - Gen2的标记和清扫在后台线程并发执行
// - Gen0和Gen1的GC可以在Background GC期间进行
// - 大幅减少Gen2 GC的暂停时间

// Background GC的时间线:
//
// 应用:  ████████████████████████████████████
// Gen0:  ░░██░░░░░░██░░░░░░░░██░░░░░░░░██░░  (频繁短暂停)
// Gen2:  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  (并发标记)
// Gen2:  ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██  (仅初始/重新标记STW)

7.3 Unity/Mono的GC策略

复制代码
// Unity使用的GC策略取决于运行时:
//
// 1. Mono + Boehm GC (旧版Unity)
//    - 非增量、非并发的标记-清扫GC
//    - 全程STW
//    - 暂停时间长
//    - 不支持并发
//
// 2. Mono + SGen GC (较新Unity)
//    - 分代GC
//    - Gen0/Gen1: STW + 并行
//    - Gen2: 可以配置为并发标记
//    - 支持写屏障
//
// 3. IL2CPP
//    - 使用Boehm GC
//    - 非增量、非并发
//    - 但可以通过配置启用增量模式
//
// Unity增量GC (Incremental GC):
// - Unity 2019.1+引入
// - 基于Boehm GC的增量模式
// - 将GC工作分摊到多帧
// - 每帧只做一小部分GC工作

// 启用Unity增量GC
// Player Settings → Other Settings → Use incremental GC
// 或脚本:
// UnityEngine.Scripting.GarbageCollector.incrementalGeneration = 1;

// 设置每帧GC预算(毫秒)
// var settings = new UnityEngine.Scripting.GarbageCollector.GCMode();
// settings.incrementalTimeSlice = 3.0f; // 每帧3ms

7.4 Unity增量GC详解

复制代码
// Unity增量GC的工作原理
// 
// 传统Boehm GC:
//   帧N: ████████████████░░░░░░░░░░░░  GC暂停50ms
//   帧N+1: ░░░░░░░░░░░░░░░░░░░░░░░░░░  正常
//
// 增量Boehm GC:
//   帧N:   ██░░░░░░░░░░░░░░  GC 3ms
//   帧N+1: ██░░░░░░░░░░░░░░  GC 3ms
//   帧N+2: ██░░░░░░░░░░░░░░  GC 3ms
//   ...
//   帧N+16:██░░░░░░░░░░░░░░  GC完成
//   总GC时间: 50ms (不变)
//   但分摊到17帧 → 每帧只暂停3ms → 不影响帧率

// Unity增量GC的限制:
// 1. 总GC时间不变(甚至略增,因为增量开销)
// 2. 如果内存压力过大,可能退化为全量GC
// 3. 增量GC期间的对象分配可能影响GC效率
// 4. 不支持并发(仍然是STW,只是分成了多次)

// 最佳实践
void OptimizeUnityGC() {
    // 1. 启用增量GC
    UnityEngine.Scripting.GarbageCollector.isIncremental = true;
    
    // 2. 控制每帧GC预算
    // 默认3ms,可根据需要调整
    // 越大 → GC完成越快 → 但每帧暂停越长
    // 越小 → 每帧暂停越短 → 但GC完成越慢
    
    // 3. 减少GC分配
    // - 使用对象池
    // - 避免在Update中分配
    // - 使用stackalloc代替小数组
    // - 使用Span<T>和Memory<T>
    
    // 4. 手动触发GC
    // 在Loading场景时触发GC
    // 避免在游戏过程中触发
    GC.Collect();
}

7.5 各运行时GC策略对比

特性 Boehm GC (Unity旧版) SGen (Mono) .NET GC Unity增量GC
分代 否 是 是 否
增量 否 否 否 是
并发 否 部分 是 否
并行 否 部分 是 否
写屏障 否 是 是 是
压缩 否 是 是 否
暂停时间 长 中等 短 短(分摊)
吞吐量 中等 中等 高 中等

八、写屏障的深入分析

8.1 写屏障的性能影响

复制代码
// 写屏障对性能的影响
// 
// 每次引用写入都需要额外操作:
// 1. 计算卡片索引
// 2. 标记卡表
// 3. 可能的内存屏障(确保可见性)
//
// 这对引用密集的代码影响很大

// 无写屏障:
void NoBarrier() {
    var a = new Node();
    a.Next = new Node();      // 直接写入
    a.Next.Next = new Node(); // 直接写入
}

// 有写屏障(概念):
void WithBarrier() {
    var a = new Node();
    WriteBarrier(ref a.Next, new Node());      // 额外开销
    WriteBarrier(ref a.Next.Next, new Node()); // 额外开销
}

// 性能差异:
// - 简单引用写入: ~1ns
// - 带写屏障的写入: ~3-5ns
// - 在引用密集的代码中可能慢2-3倍

8.2 读屏障

复制代码
// 某些GC还需要读屏障
// 读屏障: 每次读取引用时执行额外操作
//
// 用途:
// 1. 移动式GC: 读取时可能需要转发引用
// 2. Brooks指针: 每个对象有一个指向自身的指针
// 3. 并发移动GC: 读取时检查对象是否在移动
//
// .NET GC不使用读屏障(使用句柄表替代)
// 但某些GC(如Azul Zing)使用读屏障实现超低延迟

// 读屏障的性能影响比写屏障更大
// 因为读操作远多于写操作
// 所以大多数GC尽量避免读屏障

九、实际应用中的选择

9.1 游戏开发中的GC策略选择

复制代码
// 游戏开发GC策略选择指南
// 
// 1. Unity + Boehm GC (IL2CPP)
//    - 启用增量GC
//    - 严格控制分配
//    - 使用对象池
//    - 适合移动平台
//
// 2. Unity + Mono (Editor)
//    - Boehm GC或SGen
//    - 开发时不太关心GC
//    - 但要避免在热路径分配
//
// 3. .NET游戏服务器
//    - 使用Server GC
//    - 配置低延迟模式
//    - 利用并发GC减少暂停
//
// 4. .NET客户端游戏
//    - Workstation GC
//    - 低延迟模式
//    - 注意Gen2 GC暂停

// .NET游戏服务器配置
void ConfigureServerGC() {
    // app.config
    // <gcServer enabled="true"/>
    // <gcConcurrent enabled="true"/>
    
    GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
    
    // 定期在安全时机触发GC
    // 例如在场景切换时
    void OnSceneChange() {
        GCSettings.LatencyMode = GCLatencyMode.Batch;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
    }
}

9.2 监控GC性能

复制代码
// 使用GC性能计数器监控
void MonitorGC() {
    // 获取GC统计信息
    Console.WriteLine($"Gen0 collections: {GC.CollectionCount(0)}");
    Console.WriteLine($"Gen1 collections: {GC.CollectionCount(1)}");
    Console.WriteLine($"Gen2 collections: {GC.CollectionCount(2)}");
    Console.WriteLine($"Total allocated: {GC.GetTotalAllocatedBytes()}");
    
    // 使用PerformanceCounter
    using var gen0Counter = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName);
    using var gen2Counter = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName);
    using var pauseTime = new PerformanceCounter(".NET CLR Memory", "% Time in GC", Process.GetCurrentProcess().ProcessName);
    
    Console.WriteLine($"Time in GC: {pauseTime.NextValue()}%");
}

// 使用ETW事件追踪GC
// .NET提供详细的GC ETW事件
// 可以使用PerfView或dotnet-trace分析

十、总结

增量式GC、并发式GC和并行式GC是减少GC暂停时间的三种核心策略:

  1. 增量式GC:将一次长GC拆分成多次短GC,通过写屏障保证正确性。适合实时系统,但总GC时间更长。

  2. 并发式GC:让GC的大部分工作与应用线程同时运行,只有关键阶段需要STW。暂停时间最短,但实现最复杂。

  3. 并行式GC:使用多线程加速GC工作,减少STW持续时间。吞吐量最高,但暂停时间不如并发式短。

  4. 混合策略:实际GC系统通常混合使用多种策略。.NET的Background GC结合了并发和并行,Unity的增量GC是增量式的简化版本。

  5. 写屏障是增量式和并发式GC的基础,它记录引用修改,确保GC正确性,但带来性能开销。

  6. 选择策略需要根据应用场景权衡:游戏需要低延迟(增量/并发),服务器需要高吞吐(并行),实时系统需要可预测的暂停(增量)。

下一篇 :02-07 GC暂停时间的本质

相关推荐
对空六课1 小时前
无头浏览器访问对跳出率的过滤规则
服务器·前端·算法·数据分析
有同事要进步1 小时前
leecode第一题-两数字之和(打卡)
算法
程序员清风1 小时前
二叉搜索树:查找、插入与删除
算法
HRTOS1 小时前
HRTOS 内核 BUG 修复:任务等待对象未初始化导致的异常唤醒问题
单片机·嵌入式硬件·算法·51单片机
XMAIPC_Robot1 小时前
国产FPGA替代Xilinx在储能电力采集场景可行性评估与移植经验
人工智能·算法·机器学习·fpga开发·rk3588+fpga
Vertira2 小时前
C# 函数封装成 DLL,
开发语言·c#
开开心心就好2 小时前
一元二次方程求解工具,答案带详细步骤
java·服务器·开发语言·算法·pdf·ocr·perl
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出套碗游戏中的“幸运碗“——只出现一次的数字
c语言·数据结构·算法·数组·位运算·时间复杂度·空间复杂度
yi0112 小时前
LeetCode 3. 无重复字符的最长子串|不定长滑动窗口:什么时候更新最大值?
笔记·python·算法·leetcode·双指针·滑动窗口