02-07-原理篇-GC暂停时间的本质
GC 暂停时间的本质篇章02-原理篇阅读时间约 35 分钟前置知识了解 GC 触发条件一、引言GC暂停Stop-The-World, STW是所有使用GC的运行时都绕不开的话题。在游戏开发中一次50ms的GC暂停意味着丢失3帧画面在交易系统中一次100ms的暂停可能导致订单超时。理解GC暂停的本质——它从何而来、由什么组成、如何测量、如何减少——是性能优化的核心知识。本文将深入剖析GC暂停的每一个来源从安全点机制到各阶段的耗时分析再到实际的测量和优化策略。无论你使用Unity还是.NET理解这些原理都能帮助你更好地控制GC暂停时间。二、Stop-The-World 详解2.1 什么是 STWStop-The-World是指GC运行时暂停所有应用线程的状态。在STW期间应用线程完全停止不执行任何代码。这是GC保证堆一致性的最简单方式。正常执行: 线程1: ████████████████████████████ 线程2: ████████████████████████████ 线程3: ████████████████████████████ GC: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ STW暂停: 线程1: ████████░░░░░░░░░░░░████████ 线程2: ████████░░░░░░░░░░░░████████ 线程3: ████████░░░░░░░░░░░░████████ GC: ░░░░░░░░████████████░░░░░░░░ ↑ GC工作 ↑2.2 STW 的触发时机GC在以下情况会触发STW// 1. 显式触发 GC.Collect(); // 触发GC需要STW GC.Collect(generation); // 指定代的GC GC.Collect(generation, mode); // 指定模式 // 2. 隐式触发分配时 // 当分配新对象时如果对应代的预算超限 var obj new BigObject(); // 可能触发GC // 3. 内存压力触发 // 操作系统内存不足时 // .NET会收到低内存通知并触发GC // 4. 定时触发某些GC实现 // 后台GC定期检查是否需要GC // 5. 大对象分配 // LOH分配可能触发Gen2 GC var bigArray new int[100_000]; // 可能触发GC2.3 STW的启动过程// STW不是瞬间完成的它有一个启动过程: // // 1. GC线程发起STW请求 // 2. 所有应用线程到达安全点 // 3. 所有应用线程暂停 // 4. GC开始工作 // // 从发起请求到所有线程暂停的时间 STW启动延迟 // 这个延迟取决于线程到达安全点的速度 // 时间线: // GC请求: ░░░░░░░░█░░░░░░░░░░░░░░░░░░░ // 线程1: ████████░░░░░░░░░░░░████████ (快速到达安全点) // 线程2: ████████████░░░░░░░░████████ (慢速到达安全点) // 线程3: ████████████████░░░░████████ (最慢到达安全点) // ↑ // 所有线程到达安全点 // GC开始工作 // 问题: 如果某个线程长时间不到达安全点 // → STW启动延迟很长 → 实际暂停时间远超预期三、安全点Safepoint3.1 安全点机制安全点是指应用线程可以安全暂停的代码位置。在这些位置线程的所有引用都在已知状态GC可以安全地扫描和修改对象。// 安全点的特征: // 1. 所有引用变量都在已知位置栈/寄存器中可被枚举 // 2. 没有正在进行的引用计算 // 3. 线程可以安全地暂停并等待GC完成 // JIT编译器在以下位置插入安全点: // 1. 方法入口 // 2. 方法出口 // 3. 循环回边每次循环迭代 // 4. 方法调用点 // 5. 分配新对象前 // 安全点检查的伪代码: void CompiledMethod() { // 方法入口 → 安全点 if (GCRequested) PauseForGC(); while (condition) { // 循环回边 → 安全点 if (GCRequested) PauseForGC(); DoWork(); } // 方法出口 → 安全点 if (GCRequested) PauseForGC(); }3.2 安全点的实现// .NET中的安全点实现 // // JIT编译器在生成的机器码中插入安全点检查 // 检查方式: // 1. 读取一个全局标志GC触发标志 // 2. 如果标志被设置 → 跳转到暂停代码 // 3. 如果标志未设置 → 继续执行 // 伪汇编代码: // mov eax, [gc_poll_flag] ; 读取GC标志 // test eax, eax ; 检查是否需要GC // jnz suspend_code ; 如果需要跳转到暂停 // ; ... 继续正常执行 ... // // suspend_code: // ; 保存所有引用到栈上的GC信息表 // ; 调用GC暂停函数 // ; 等待GC完成 // ; 恢复执行 // 安全点检查的开销: // - 每次检查约1-2个CPU周期 // - 对性能影响极小 // - 但在高频循环中可能累积3.3 安全区域安全点解决了线程在运行时的暂停问题但对于阻塞状态的线程如等待I/O、睡眠、等待锁安全点无法生效。这时需要安全区域。// 安全区域: 一段代码区域在其中引用不会改变 // 进入安全区域时线程通知GC我在安全区域 // 离开安全区域时线程检查是否需要暂停 // .NET中的安全区域: // 1. 阻塞操作Thread.Sleep, WaitOne等 // 2. I/O操作 // 3. P/Invoke调用调用非托管代码时 // 4. 线程池等待 // 伪代码: void ThreadSleep(int ms) { // 进入安全区域 EnterSafeRegion(); // 执行睡眠 Sleep(ms); // 离开安全区域 LeaveSafeRegion(); // 如果GC正在进行 → 等待GC完成 } void EnterSafeRegion() { // 通知GC: 这个线程在安全区域 // GC可以忽略这个线程它的引用不会改变 Thread.CurrentThread.InSafeRegion true; } void LeaveSafeRegion() { Thread.CurrentThread.InSafeRegion false; // 检查GC是否正在进行 if (GCInProgress) { WaitForGC(); // 阻塞直到GC完成 } }3.4 安全点的问题// 安全点的问题: 长时间运行的循环 // // 如果一个循环没有安全点如某些内联的紧密循环 // → GC等待线程到达安全点 → STW启动延迟 // 问题示例: 大循环没有安全点 void LongLoop() { long sum 0; for (long i 0; i long.MaxValue; i) { sum i; // 紧密循环可能没有安全点 } // 如果GC在此期间触发 → 等待循环结束 → 长暂停 } // .NET的改进: // .NET Core 3.0在所有循环回边插入安全点 // 之前某些简单循环可能被优化掉安全点 // 手动插入安全点如果需要 void LongLoopWithSafepoint() { long sum 0; for (long i 0; i long.MaxValue; i) { sum i; // 每10000次迭代检查一次 if (i % 10000 0) { Thread.Sleep(0); // 触发安全点检查 } } }四、GC 暂停的来源分析4.1 标记阶段耗时标记阶段是GC的核心它从GC根出发遍历整个对象图标记所有可达对象。// 标记阶段的工作: // 1. 枚举所有GC根栈变量、静态字段、句柄等 // 2. 从根出发深度优先/广度优先遍历对象图 // 3. 标记每个访问到的对象 // 标记阶段的耗时来源: // 1. GC根数量 → 根越多初始扫描越慢 // 2. 对象图大小 → 对象越多遍历越慢 // 3. 对象引用密度 → 引用越多遍历越深 // 4. 缓存局部性 → 对象分散在内存中 → 缓存未命中 // 标记阶段的优化: // 1. 分代GC: 只标记年轻代 → 减少标记范围 // 2. 并发标记: 与应用线程并发 → 减少STW // 3. 卡表: 只重新扫描修改过的区域 // 4. 位图标记: 使用位图而非对象头 → 减少内存访问 // 标记阶段的时间估算: // 假设: // - 1GB堆 // - 平均对象大小50字节 // - 约2000万个对象 // - 每个对象标记约100ns含缓存未命中 // - 总标记时间: 2000万 × 100ns 2秒 // // 但分代GC只标记Gen0~20MB: // - 约40万个对象 // - 总标记时间: 40万 × 100ns 40ms // → 分代GC大幅减少标记时间4.2 清扫阶段耗时清扫阶段释放未被标记的对象回收内存。// 清扫阶段的工作: // 1. 遍历堆中的所有对象 // 2. 检查标记位 // 3. 未标记的对象 → 加入空闲列表/标记为可用 // 4. 已标记的对象 → 清除标记位为下次GC准备 // 清扫阶段的耗时来源: // 1. 堆大小 → 需要遍历整个堆 // 2. 碎片化 → 需要合并空闲块 // 3. 空闲列表维护 → 更新空闲列表 // 清扫阶段的优化: // 1. 并发清扫: 与应用线程并发执行 // 2. 分代清扫: 只清扫年轻代 // 3. 空闲列表优化: 使用分层空闲列表 // 4. 标记位图: 使用位图快速查找未标记对象 // .NET的清扫策略: // - Gen0/Gen1: 清扫 压缩移动对象 // - Gen2: 清扫不压缩除非碎片化严重 // - LOH: 通常不压缩 // 清扫时间估算: // - 遍历1GB堆: ~50-100ms // - 分代GC只清扫Gen0~20MB: ~1-2ms4.3 压缩阶段耗时压缩阶段Compaction移动对象以消除碎片这是GC中最耗时的操作。// 压缩阶段的工作: // 1. 计算每个存活对象的新位置 // 2. 移动对象到新位置 // 3. 更新所有引用指向新位置 // 4. 更新GC根 // 压缩阶段的耗时来源: // 1. 存活对象数量 → 需要移动的对象数 // 2. 引用更新 → 需要更新的引用数 // 3. 内存拷贝 → 移动对象的数据量 // 4. 缓存影响 → 移动导致缓存失效 // 压缩 vs 不压缩 // // 压缩: // 优点: 消除碎片分配速度快 // 缺点: 耗时长暂停时间大 // // 不压缩: // 优点: 暂停时间短 // 缺点: 碎片化分配速度慢需要搜索空闲列表 // .NET的压缩策略: // - Gen0: 总是压缩对象少速度快 // - Gen1: 总是压缩 // - Gen2: 根据碎片化程度决定 // - LOH: 默认不压缩.NET 4.5.1可配置 // 手动控制LOH压缩 void CompactLargeObjectHeap() { GCSettings.LargeObjectHeapCompactionMode GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); // 压缩后恢复默认 GCSettings.LargeObjectHeapCompactionMode GCLargeObjectHeapCompactionMode.Default; }4.4 各阶段暂停时间占比典型Gen0 GC暂停时间分解 (约5ms): ┌──────────┬──────────┬──────────┐ │ 根扫描 │ 标记 │ 清扫压缩 │ │ ~1ms │ ~2ms │ ~2ms │ │ 20% │ 40% │ 40% │ └──────────┴──────────┴──────────┘ 典型Gen2 GC暂停时间分解 (约50-200ms): ┌──────────┬──────────┬──────────┬──────────┐ │ 初始标记 │ 标记 │ 清扫 │ 压缩 │ │ ~5ms │ ~20ms │ ~10ms │ ~50ms │ │ 7% │ 28% │ 14% │ 51% │ └──────────┴──────────┴──────────┴──────────┘ 并发Gen2 GC暂停时间分解 (约10-20ms): ┌──────────┬──────────────────────┬──────────┐ │ 初始标记 │ (并发标记, 不计入) │ 重新标记 │ │ ~5ms │ │ ~5ms │ │ 50% │ │ 50% │ └──────────┴──────────────────────┴──────────┘五、如何测量暂停时间5.1 使用GC通知API// .NET提供GC通知API可以测量GC暂停时间 class GCProfiler { private static Stopwatch _sw new(); private static long _totalPauseTime 0; private static int _gcCount 0; public static void Start() { GC.RegisterForFullGCNotification(10, 10); // 在另一个线程中等待GC通知 Task.Run(() { while (true) { if (GC.WaitForFullGCApproach() GCNotificationStatus.Succeeded) { _sw.Restart(); } if (GC.WaitForFullGCComplete() GCNotificationStatus.Succeeded) { _sw.Stop(); _totalPauseTime _sw.ElapsedMilliseconds; _gcCount; Console.WriteLine($GC #{_gcCount}: {_sw.ElapsedMilliseconds}ms); Console.WriteLine($Total pause: {_totalPauseTime}ms); } } }); } }5.2 使用ETW事件// .NET的ETWEvent Tracing for Windows提供详细的GC事件 // 可以使用PerfView或dotnet-trace工具分析 // 使用dotnet-trace命令行: // dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime --process-id pid // // 关键GC事件: // - GCStart: GC开始包含代、原因、类型 // - GCEnd: GC结束 // - GCSuspendEEStart: STW开始 // - GCSuspendEEEnd: STW完成 // - GCRestartEEStart: 恢复执行开始 // - GCRestartEEEnd: 恢复执行完成 // // 暂停时间 GCRestartEEEnd - GCSuspendEEStart5.3 使用Unity Profiler// Unity中测量GC暂停 // // 1. 使用Profiler // Window → Analysis → Profiler → Memory // 可以看到GC Alloc和GC Collect事件 // // 2. 使用脚本测量 class UnityGCProfiler : MonoBehaviour { private float _lastFrameTime; private float _maxFrameTime; void Update() { float frameTime Time.realtimeSinceStartup - _lastFrameTime; _lastFrameTime Time.realtimeSinceStartup; if (frameTime _maxFrameTime) { _maxFrameTime frameTime; Debug.Log($Max frame time: {_maxFrameTime * 1000}ms); } // 检测GC导致的帧时间飙升 if (frameTime 0.020f) { // 20ms Debug.LogWarning($Frame spike: {frameTime * 1000}ms (possible GC)); } } void OnGUI() { GUILayout.Label($Max frame: {_maxFrameTime * 1000:F1}ms); GUILayout.Label($GC alloc/frame: {UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024}KB); } }5.4 使用性能计数器// 使用.NET性能计数器监控GC class GCPerformanceMonitor { public static void Monitor() { using var timeInGC new PerformanceCounter( .NET CLR Memory, % Time in GC, Process.GetCurrentProcess().ProcessName); using var gen0 new PerformanceCounter( .NET CLR Memory, # Gen 0 Collections, Process.GetCurrentProcess().ProcessName); using var gen2 new PerformanceCounter( .NET CLR Memory, # Gen 2 Collections, Process.GetCurrentProcess().ProcessName); while (true) { Console.WriteLine($Time in GC: {timeInGC.NextValue():F1}%); Console.WriteLine($Gen0: {gen0.NextValue()}, Gen2: {gen2.NextValue()}); Thread.Sleep(1000); } } }六、减少暂停的策略6.1 减少GC分配// 最根本的策略: 减少GC分配 → 减少GC触发频率 // 1. 对象池 class ObjectPoolT where T : new() { private readonly StackT _pool new(); private readonly int _maxSize; public ObjectPool(int maxSize 100) { _maxSize maxSize; } public T Get() { return _pool.Count 0 ? _pool.Pop() : new T(); } public void Return(T item) { if (_pool.Count _maxSize) { _pool.Push(item); } } } // 使用对象池 class Bullet : MonoBehaviour { private static ObjectPoolBullet _pool new(200); public static Bullet Spawn(Vector3 pos) { var bullet _pool.Get(); bullet.transform.position pos; bullet.gameObject.SetActive(true); return bullet; } public void Despawn() { gameObject.SetActive(false); _pool.Return(this); } } // 2. 避免在热路径分配 void BadUpdate() { // 每帧分配 → 触发GC var enemies GameObject.FindGameObjectsWithTag(Enemy); foreach (var e in enemies) { /* ... */ } } private ListGameObject _enemyCache new(); void GoodUpdate() { // 复用列表 _enemyCache.Clear(); GameObject.FindGameObjectsWithTag(Enemy, _enemyCache); foreach (var e in _enemyCache) { /* ... */ } } // 3. 使用stackalloc unsafe void ProcessData() { // 栈上分配不触发GC int* buffer stackalloc int[256]; for (int i 0; i 256; i) { buffer[i] i; } } // 4. 使用Span和ArrayPool void ProcessWithSpan() { // 从ArrayPool租用数组 int[] buffer ArrayPoolint.Shared.Rent(1024); try { var span buffer.AsSpan(0, 1024); // 使用span... } finally { ArrayPoolint.Shared.Return(buffer); } }6.2 控制GC触发时机// 在安全时机手动触发GC class GCController { // 在Loading场景时触发GC public static void CollectDuringLoading() { // 切换到高吞吐模式 var oldMode GCSettings.LatencyMode; GCSettings.LatencyMode GCLatencyMode.Batch; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // 再次收集终结后的对象 GCSettings.LatencyMode oldMode; } // 在帧间隙触发增量GC public static void CollectBetweenFrames() { // Unity增量GC: 每帧自动做一小部分 // 也可以手动控制 if (GC.GetTotalMemory(false) THRESHOLD) { GC.Collect(0); // 只收集Gen0 } } private const long THRESHOLD 100 * 1024 * 1024; // 100MB } // Unity中在场景切换时触发GC class SceneLoader : MonoBehaviour { IEnumerator LoadSceneAsync(string sceneName) { // 先触发GC GCController.CollectDuringLoading(); // 等待一帧 yield return null; // 加载场景 var op SceneManager.LoadSceneAsync(sceneName); yield return op; } }6.3 使用低延迟GC模式// .NET的低延迟GC模式 class LowLatencyGC { public static void Configure() { // SustainedLowLatency: 持续低延迟 // - 避免Gen2 GC // - Gen0和Gen1正常GC // - 适合游戏和实时应用 GCSettings.LatencyMode GCLatencyMode.SustainedLowLatency; // 注意: SustainedLowLatency模式下 // 如果内存压力过大仍会触发Gen2 GC // 需要配合减少分配使用 } // 临时低延迟区域 public static void RunLowLatency(Action action) { var oldMode GCSettings.LatencyMode; try { GCSettings.LatencyMode GCLatencyMode.LowLatency; action(); } finally { GCSettings.LatencyMode oldMode; } } // 在关键帧使用低延迟 void Update() { // 检测是否在关键帧 if (IsCriticalFrame()) { LowLatencyGC.RunLowLatency(() { // 关键帧逻辑 // 期间不会触发Gen2 GC }); } } bool IsCriticalFrame() { // 例如: 战斗中、过场动画中 return inBattle || inCutscene; } }6.4 固定对象避免移动// 使用fixed/GCHandle.Alloc固定对象 // 防止GC移动对象 → 减少压缩开销 // 1. fixed语句 void ProcessArray(int[] array) { fixed (int* ptr array) { // array不会被GC移动 for (int i 0; i array.Length; i) { ptr[i] * 2; } } // fixed块结束后对象可以被移动 } // 2. GCHandle.Alloc with Pinned class PinnedBuffer { private GCHandle _handle; private byte[] _buffer; public PinnedBuffer(int size) { _buffer new byte[size]; _handle GCHandle.Alloc(_buffer, GCHandleType.Pinned); // _buffer在_handle释放前不会被GC移动 } public IntPtr Pointer _handle.AddrOfPinnedObject(); public void Free() { _handle.Free(); // 释放后可以被移动 } } // 注意: 固定对象会影响GC的压缩效率 // 不要长期固定大量对象6.5 LOH优化// 大对象堆LOH优化 // LOH中的对象 85,000字节 // LOH不压缩 → 碎片化 // LOH GC需要Gen2 GC → 暂停时间长 // 策略1: 避免大对象 // - 使用多个小对象代替一个大对象 // - 使用ArrayPool租用大数组 // 策略2: 复用大对象 class LargeBufferManager { private static readonly ArrayPoolbyte _pool ArrayPoolbyte.Shared; public static byte[] GetBuffer(int size) { return _pool.Rent(size); } public static void ReturnBuffer(byte[] buffer) { _pool.Return(buffer); } } // 策略3: 手动压缩LOH void CompactLOH() { GCSettings.LargeObjectHeapCompactionMode GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); // 压缩后恢复默认 GCSettings.LargeObjectHeapCompactionMode GCLargeObjectHeapCompactionMode.Default; } // 策略4: 使用NativeMemory分配超大对象 unsafe void AllocateLargeBuffer() { // 直接分配非托管内存不受GC管理 byte* buffer (byte*)NativeMemory.Alloc(1024 * 1024 * 100); // 100MB try { // 使用buffer... } finally { NativeMemory.Free(buffer); } }七、Unity中的GC暂停7.1 Unity GC暂停的特殊性// Unity的GC暂停有特殊影响: // 1. 主线程是游戏循环 → GC暂停直接影响帧率 // 2. Boehm GC不支持并发 → 全程STW // 3. 没有分代 → 每次GC都是Full GC // 4. 没有压缩 → 碎片化问题 // Unity GC暂停时间估算: // - 小堆(50MB): ~10-30ms // - 中堆(200MB): ~50-100ms // - 大堆(500MB): ~100-300ms // // 60FPS游戏每帧16.67ms // → 任何超过16ms的GC暂停都会导致掉帧 // Unity增量GC改善: // - 将50ms的GC暂停分摊到17帧 // - 每帧约3ms → 不影响帧率 // - 但总GC时间不变甚至略增7.2 Unity GC暂停的测量// 使用Unity Profiler测量GC暂停 class GCProfiler : MonoBehaviour { private struct GCStats { public long totalMemory; public long allocatedMemory; public float frameTime; } private GCStats _lastStats; private float _maxSpike; void Update() { float frameTime Time.realtimeSinceStartup - _lastStats.frameTime; long currentMem GC.GetTotalMemory(false); long allocDelta currentMem - _lastStats.totalMemory; // 检测GC暂停内存突然减少 帧时间飙升 if (allocDelta 0 frameTime 0.020f) { Debug.LogWarning($GC detected! Pause: {frameTime * 1000:F1}ms, $Memory freed: {-allocDelta / 1024}KB); if (frameTime _maxSpike) { _maxSpike frameTime; } } _lastStats new GCStats { totalMemory currentMem, allocatedMemory GC.GetTotalAllocatedMemory(), frameTime Time.realtimeSinceStartup }; } void OnGUI() { GUILayout.BeginArea(new Rect(10, 10, 300, 100)); GUILayout.Label($Max GC Spike: {_maxSpike * 1000:F1}ms); GUILayout.Label($Total Memory: {GC.GetTotalMemory(false) / 1024 / 1024}MB); GUILayout.Label($GC Mode: {(UnityEngine.Scripting.GarbageCollector.isIncremental ? Incremental : Full)}); GUILayout.EndArea(); } }八、暂停时间优化总结8.1 优化策略对比策略效果适用场景代价减少分配减少GC频率所有场景代码复杂度增加对象池减少分配频繁创建/销毁内存占用增加增量GC分摊暂停Unity实时总GC时间略增并发GC减少STW.NET服务写屏障开销低延迟模式延迟Gen2 GC.NET实时内存占用增加手动触发GC控制GC时机所有场景需要合适时机LOH优化减少Gen2 GC大对象场景代码复杂度8.2 暂停时间目标不同应用的暂停时间目标: 移动游戏 (60FPS): 目标: 5ms (Gen0), 16ms (Gen2) 策略: 增量GC 对象池 减少分配 PC游戏 (60FPS): 目标: 10ms (Gen0), 30ms (Gen2) 策略: Server GC 低延迟 对象池 VR游戏 (90FPS): 目标: 3ms (任何GC) 策略: 极端减少分配 增量GC Web服务器: 目标: 50ms (Gen2) 策略: Server GC 并发GC 交易系统: 目标: 1ms (任何GC) 策略: 几乎零分配 System.GC.TryStartNoGCRegion8.3 使用NoGCRegion// .NET提供NoGCRegion API在关键区域禁止GC void CriticalOperation() { // 尝试进入NoGC区域 // 预留足够的内存确保期间不需要GC if (GC.TryStartNoGCRegion(10 * 1024 * 1024)) { // 10MB try { // 在这个区域内不会触发GC // 适合关键帧或实时操作 DoCriticalWork(); } finally { GC.EndNoGCRegion(); } } else { // 无法进入NoGC区域内存不足 // 降级处理 DoNormalWork(); } } // Unity中没有NoGCRegion API // 但可以通过减少分配达到类似效果九、总结GC暂停时间的本质是GC为了保证堆一致性而暂停应用线程的时间。理解暂停的来源和组成是优化GC性能的基础安全点是STW的基础线程必须在安全点才能暂停。长时间运行的循环可能导致STW启动延迟。标记阶段耗时取决于对象图大小分代GC通过缩小标记范围大幅减少耗时。清扫阶段耗时取决于堆大小并发清扫可以减少STW。压缩阶段是最耗时的操作移动对象和更新引用开销大。避免不必要的压缩是减少暂停的关键。测量暂停使用GC通知API、ETW事件、Unity Profiler等工具。减少暂停的核心策略减少分配、控制GC时机、使用低延迟模式、优化LOH。Unity特殊注意Boehm GC不支持并发增量GC是主要的优化手段。配合对象池和减少分配可以达到较好的效果。下一篇02-08 GC的视野边界