二十一、托管堆与垃圾回收(ManagedHeapGarbage)
CLR #cg
托管堆与垃圾回收(ManagedHeap&GarbageCollection)
内存管理是确保应用程序性能和稳定性的关键。CLR(公共语言运行时)通过其强大的垃圾回收(Garbage Collection, GC)机制,自动管理内存分配和释放,极大地简化了开发者的工作。
1. 什么是托管堆和垃圾回收?
CLR 的托管堆是一个内存区域,用于存储 .NET 应用程序中的对象。垃圾回收器(GC)负责跟踪和管理这些对象的生命周期,自动回收不再使用的内存。CLR 的 GC 采用 标记-清除 算法,具体步骤如下:
- 标记阶段:GC 从应用程序的“根”(如局部变量、静态字段)开始,遍历所有可达对象,将其标记为“可达”(reachable)。GC 会检查堆中的对象 A、C、D、F,发现对象 D 引用了对象 H,因此 H 也被标记为可达。
- 清除阶段:未标记的对象被视为不可达(unreachable),其内存将被回收。回收后,第 0 代不包含任何对象,释放的内存可供新对象分配。
这种机制确保了应用程序只需关注业务逻辑,而无需手动管理内存释放。
2. 分代垃圾回收:优化性能的关键
CLR 的垃圾回收器采用 分代垃圾回收(generational garbage collector),将对象分为三代(第 0 代、第 1 代、第 2 代),以提高回收效率。
- 第 0 代:新分配的对象位于第 0 代,通常是短期存活对象。如果第 0 代满了,GC 会触发回收,回收不可达对象并将存活对象提升到第 1 代。
- 第 1 代:存活过一次回收的对象进入第 1 代。如果第 1 代满了,GC 会检查第 0 代和第 1 代,存活对象提升到第 2 代。
- 第 2 代:长期存活的对象位于第 2 代。第 2 代回收较少发生,因为这些对象通常是应用程序的核心组件。
分代机制基于一个假设:新分配的对象通常寿命较短,而存活时间长的对象更可能继续存活。通过减少对第 2 代的检查,GC 显著提高了性能。GC 会根据内存负载动态调整各代的预算,进一步优化回收效率。
3. 性能优化:主动控制垃圾回收
虽然 GC 自动管理内存,但开发者可以通过一些方法优化性能:
-
强制垃圾回收:
System.GC.Collect方法允许强制触发垃圾回收。可以通过指定代数(int32 generation)和模式(GCCollectionMode)来控制回收范围。例如:GC.Collect(2, GCCollectionMode.Optimized, true);建议在非重复性事件(如保存工作状态后)调用
Collect,但应谨慎使用,以免干扰 GC 的自动优化。 -
LowLatency 模式:在低延迟场景(如实时应用程序)中,可以启用
LowLatency模式,减少 GC 暂停时间。但此模式可能增加OutOfMemoryException的风险,因此应尽量缩短使用时间,并通过Interlocked方法安全切换模式:GCLatencyMode oldMode = GCSettings.LatencyMode; try { GCSettings.LatencyMode = GCLatencyMode.LowLatency; // 执行低延迟操作 } finally { GCSettings.LatencyMode = oldMode; } -
GC 通知:
GCNotification类允许监听第 0 代或第 2 代的回收事件,帮助开发者分析内存使用情况:GCNotification.GCDone += (generation) => { Console.WriteLine($"GC 完成,第 {generation} 代"); };
4. 资源清理:Finalize 与 SafeHandle
除了内存管理,CLR 还需要处理非托管资源(如文件句柄、数据库连接)的清理。
-
避免直接重写 Finalize:
Object.Finalize方法用于清理非托管资源,但直接重写复杂且易出错。建议使用SafeHandle或CriticalHandle。 -
SafeHandle:
System.Runtime.InteropServices.SafeHandle是一个抽象类,封装了非托管资源的安全释放。开发者应从SafeHandle派生自定义类,并实现ReleaseHandle方法:public class MySafeHandle : SafeHandle { public MySafeHandle() : base(IntPtr.Zero, true) { } public override bool IsInvalid => handle == IntPtr.Zero; protected override bool ReleaseHandle() { // 释放非托管资源 return true; } } -
CriticalHandle:
CriticalHandle适用于需要更高可靠性的场景,但功能较少,通常不直接使用。
通过 SafeHandle,CLR 确保资源在对象不可达时安全释放,同时支持 IDisposable 模式以显式清理。
5. 调试与分析工具
- ETW(Event Tracing for Windows):用于跟踪 CLR 事件,分析内存分配和 GC 行为。
- SOS Debugging Extension:通过
SOS.dll,开发者可以检查托管堆状态,定位内存泄漏。
这些工具对于优化大型 .NET 应用程序尤为重要。
6. 最佳实践
CLR 的垃圾回收机制通过托管堆和分代回收,极大地简化了内存管理。开发者可以通过以下实践优化性能:
- 尽量减少大对象分配,优先使用对象池。
- 在性能敏感场景下,谨慎使用
GC.Collect和LowLatency模式。 - 使用
SafeHandle管理非托管资源,避免直接操作Finalize。 - 利用
GCNotification和 ETW 工具监控内存使用。
7.Unity 开发实践中的建议
减少对象分配以降低 GC 压力
-
章节依据:第21章提到,分代垃圾回收(generational GC)将新对象分配到第 0 代,频繁分配会导致第 0 代填满,触发 GC。
-
实践建议:
- 使用 对象池(Object Pooling)来复用 GameObject 或组件,避免频繁实例化。例如,子弹、敌人等高频创建的对象应通过对象池管理:
public class BulletPool : MonoBehaviour { private List<GameObject> pool = new List<GameObject>(); public GameObject bulletPrefab; public GameObject GetBullet() { foreach (var bullet in pool) { if (!bullet.activeInHierarchy) return bullet; } var newBullet = Instantiate(bulletPrefab); pool.Add(newBullet); return newBullet; } } - 避免在
Update等高频调用的方法中创建临时对象(如字符串拼接或new关键字),因为这些对象会快速填满第 0 代。// 避免 void Update() { string fps = "FPS: " + (1f / Time.deltaTime); } // 优化 private StringBuilder sb = new StringBuilder(); void Update() { sb.Clear().Append("FPS: ").Append(1f / Time.deltaTime); }
- 使用 对象池(Object Pooling)来复用 GameObject 或组件,避免频繁实例化。例如,子弹、敌人等高频创建的对象应通过对象池管理:
谨慎使用值类型和引用类型
-
章节依据:第21章提到,引用类型(如类)存储在托管堆上,受 GC 管理,而值类型(如结构体)通常存储在栈上,分配更快。
-
实践建议:
- 在性能敏感场景(如粒子系统或物理计算)中使用结构体代替类,减少堆分配。例如,Unity 的
Vector3是结构体,适合高频操作。 - 避免在循环中创建大量临时引用类型对象。例如,
List<T>.Add在扩容时可能分配新数组,触发 GC。预分配足够容量:List<int> numbers = new List<int>(100); // 预分配容量
- 在性能敏感场景(如粒子系统或物理计算)中使用结构体代替类,减少堆分配。例如,Unity 的
监控和优化 GC 触发
-
章节依据:第21章提到,GC 触发会导致应用程序暂停,影响性能。
GCNotification类可用于监控 GC 事件。 -
实践建议:
- 使用 Unity 的 Profiler 检查 GC 分配和暂停时间,定位内存分配热点。
- 在非关键帧(如场景加载后)手动触发 GC,减少运行时暂停:
void OnLevelLoaded() { System.GC.Collect(); } - 在移动设备上,考虑启用 Incremental GC(Unity 2019.3+),分摊 GC 工作量,降低单帧暂停时间:
// 在 Unity 脚本中启用增量 GC(需检查 Unity 版本支持) #if UNITY_2019_3_OR_NEWER UnityEngine.Rendering.ScriptableRenderContext.GCSettings.isIncremental = true; #endif
管理非托管资源
-
章节依据:第21章强调使用
SafeHandle管理非托管资源(如文件句柄),避免直接重写Finalize。 -
实践建议:
- 在 Unity 中使用插件(如调用原生 C++ 代码)时,确保通过
SafeHandle或IDisposable模式释放非托管资源。例如,处理纹理或音频句柄时:public class NativePlugin : SafeHandle { public NativePlugin() : base(IntPtr.Zero, true) { } public override bool IsInvalid => handle == IntPtr.Zero; protected override bool ReleaseHandle() { // 调用原生方法释放资源 NativeMethods.ReleaseResource(handle); return true; } } - 实现
IDisposable模式,确保资源在OnDestroy或OnDisable时释放:public class ResourceHolder : MonoBehaviour, IDisposable { private bool disposed = false; public void Dispose() { if (!disposed) { // 释放资源 disposed = true; } } void OnDestroy() { Dispose(); } }
- 在 Unity 中使用插件(如调用原生 C++ 代码)时,确保通过
优化大对象分配
-
章节依据:第21章提到,大对象(>85KB)直接分配到第 2 代,增加 GC 负担。
-
实践建议:
- 避免创建大数组或大型纹理对象,尽量分割为较小的块。例如,加载大纹理时使用压缩格式或分块加载。
- 使用 Unity 的 Addressables 系统异步加载资源,减少一次性内存分配:
async void LoadAssetAsync() { var handle = Addressables.LoadAssetAsync<Texture2D>("texture"); await handle.Task; Texture2D texture = handle.Result; }
低延迟场景优化
- 章节依据:第21章提到
LowLatency模式可减少 GC 暂停,但需谨慎使用。 - 实践建议:
- 在 Unity 的实时多人游戏或 VR 应用中,临时启用
LowLatency模式以确保低延迟:void StartCriticalSection() { var oldMode = System.Runtime.GCSettings.LatencyMode; System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.LowLatency; // 执行关键逻辑 System.Runtime.GCSettings.LatencyMode = oldMode; } - 避免在
LowLatency模式下分配大对象,以免触发OutOfMemoryException。
- 在 Unity 的实时多人游戏或 VR 应用中,临时启用
8.Unity 面试题及答案
-
问题:Unity 中的垃圾回收如何影响游戏性能?如何检测 GC 相关问题?
- 答案: Unity 使用 Mono(或 IL2CPP)的垃圾回收器,基于分代 GC(第 0、1、2 代)。GC 触发时会暂停应用程序,扫描托管堆,回收不可达对象,可能导致帧率下降(卡顿)。频繁分配短期对象(如在
Update中创建字符串)会填满第 0 代,增加 GC 频率。
检测方法:- 使用 Unity Profiler 的 Memory 视图,检查 GC 分配峰值和暂停时间。
- 启用
GCNotification监听 GC 事件,记录第 0 代或第 2 代回收频率(参考第21章)。
优化方法: - 使用对象池复用 GameObject。
- 避免在高频方法中分配内存(如字符串拼接)。
- 在非关键时刻手动调用
System.GC.Collect(),如场景切换后。
- 答案: Unity 使用 Mono(或 IL2CPP)的垃圾回收器,基于分代 GC(第 0、1、2 代)。GC 触发时会暂停应用程序,扫描托管堆,回收不可达对象,可能导致帧率下降(卡顿)。频繁分配短期对象(如在
-
问题:如何在 Unity 中减少垃圾回收的频率?
-
答案: 减少 GC 频率的关键是降低托管堆的对象分配,尤其是在第 0 代(参考第21章的分代 GC)。
-
对象池:复用 GameObject 或组件,避免频繁
Instantiate和Destroy。 -
值类型:使用结构体(如
Vector3)代替类,减少堆分配。 -
预分配容器:初始化
List<T>或数组时指定足够容量,避免扩容分配。 -
字符串优化:使用
StringBuilder代替字符串拼接。 -
增量 GC:在 Unity 2019.3+ 中启用增量 GC,分摊 GC 工作量,减少单帧暂停。
List<GameObject> enemies = new List<GameObject>(50); // 预分配容量 StringBuilder sb = new StringBuilder(); // 避免字符串分配
-
-
-
问题:什么是 SafeHandle?在 Unity 中如何使用它管理非托管资源?
-
答案:
SafeHandle是 .NET 提供的一个抽象类,用于安全管理非托管资源(如文件句柄、原生插件资源),避免直接重写Finalize(参考第21章)。它实现了IDisposable和终结器,确保资源在对象不可达时安全释放。
在 Unity 中的使用: 当调用原生插件(如 C++ 代码)时,SafeHandle可管理插件返回的句柄。例如:public class NativeTextureHandle : SafeHandle { public NativeTextureHandle() : base(IntPtr.Zero, true) { } public override bool IsInvalid => handle == IntPtr.Zero; protected override bool ReleaseHandle() { NativePlugin.ReleaseTexture(handle); return true; } }在 Unity 的
MonoBehaviour中,实现IDisposable并在OnDestroy中调用Dispose:public class TextureManager : MonoBehaviour, IDisposable { private NativeTextureHandle textureHandle; public void Dispose() => textureHandle?.Dispose(); void OnDestroy() => Dispose(); }
-
-
问题:在 Unity 中何时需要手动调用 GC.Collect?有哪些风险?
-
答案:
System.GC.Collect强制触发垃圾回收,适合在内存分配高峰后(如场景加载)清理不可达对象。
使用场景:- 场景切换后,清理上一场景的临时对象。
- 大型资源加载(如纹理)后,确保释放未引用内存。
风险: - 频繁调用会干扰 GC 的自动优化,增加 CPU 开销。
- 可能导致不必要的暂停,影响帧率。
最佳实践: - 仅在明确需要时调用(如加载完成后)。
- 结合
GCNotification监控 GC 行为,确保调用时机合理:void OnSceneLoaded() { System.GC.Collect(); Debug.Log("GC 触发完成"); }
-
-
问题:Unity 中如何处理大对象分配以减少 GC 负担?
-
答案:大对象(>85KB)直接分配到第 2 代,增加 GC 负担。在 Unity 中,大对象常见于纹理、网格或大数组。
优化方法:- 分块加载:将大纹理分割为小块,使用
Texture2D的分块加载(如Texture2D.ReadPixels)。 - Addressables:使用 Unity 的 Addressables 系统异步加载资源,减少一次性分配:
async void LoadLargeTexture() { var handle = Addressables.LoadAssetAsync<Texture2D>("largeTexture"); await handle.Task; } - 对象池:为大对象(如粒子系统)创建对象池,复用内存。
- 压缩格式:使用压缩纹理格式(如 ETC2 或 ASTC)减少内存占用。
- 避免动态分配大数组,预估容量并复用:
byte[] buffer = new byte[100000]; // 预分配大数组
- 分块加载:将大纹理分割为小块,使用
-
❀❀❀感谢您的点赞推荐b( ̄▽ ̄)d❀❀❀
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号