
帧级预算与 GC 生命周期策略篇章08-核心技术-性能优化篇 |阅读时间约 45 分钟一、引言在前面的章节中我们逐一分析了各种 GC 分配源和零分配技术。但在实际项目中追求绝对零分配既不现实也不经济。本章将从宏观角度建立 GC 管理的工程框架帧级 GC 预算模型、对象池化策略、分代回收配合、场景加载时的 GC 控制以及完整的 GC 生命周期管理策略。这是性能优化的顶层设计章节。帧级 GC 预算的核心理念是不追求零分配而是追求可控分配。通过将 GC 分配限制在每帧的预算内让 Gen 0 GC 高效回收短命对象避免不可控的 GC 触发导致帧率抖动。在展开之前先把本章的定位说清楚。08-01 到 08-09 分析了各种具体的 GC 分配源和优化技术本章将它们整合为一个完整的工程框架。08-05 值类型篇的 struct 优化、08-07 Span 篇的零分配技术、08-08 Unity API 篇的陷阱清单都是本章预算模型的底层实现手段。二、帧级 GC 预算模型2.1 GC 预算的理论基础在深入预算模型之前先理解为什么 GC 预算能够工作。这需要从 .NET 的分代 GC 机制说起。.NET 的 GC 将堆分为三代Gen 0短命、Gen 1中等寿命、Gen 2长命。Gen 0 GC 是最快的回收操作通常耗时 1-5 毫秒。当每帧的 GC 分配量控制在合理范围内时Gen 0 GC 可以在一帧内完成回收不会跨越到下一帧。帧级 GC 预算的核心思想是将 GC 分配限制在每帧的预算内让 Gen 0 GC 在每帧结束时自然触发而不是在帧中间意外触发。这种策略的优势可预测的帧时间GC 在每帧结束时触发帧时间稳定。小批量回收每次 Gen 0 GC 回收的对象少耗时短。避免 Gen 1/Gen 2 晋升短命对象在 Gen 0 被回收不会晋升到更代。2.2 为什么需要预算GC 本身不是敌人——它是自动内存管理的必要机制。问题是不可控的 GC 触发时机导致帧率抖动。通过建立帧级 GC 预算我们将不可控的 GC转化为可控的 GC。不可控 GC 的问题 ┌─────────────────────────────────────────────────┐ │ 帧时间线60 FPS 16.67ms/帧 │ │ │ │ 帧 1: [逻辑 10ms][GC 5ms][逻辑 1.67ms] ← GC 导致掉帧 │ │ 帧 2: [逻辑 10ms][逻辑 6.67ms] │ │ 帧 3: [逻辑 10ms][GC 8ms][逻辑 0ms] ← GC 导致严重掉帧 │ │ │ │ 问题GC 触发时机不可控可能出现在任何帧 │ │ 结果帧时间从 16.67ms 飙升到 25ms帧率下降 │ └─────────────────────────────────────────────────┘ 可控 GC 的预算模型 ┌─────────────────────────────────────────────────┐ │ 帧时间线60 FPS 16.67ms/帧预算 4KB/帧 │ │ │ │ 帧 1: [逻辑 10ms][逻辑 6.67ms] ← 预算内无 GC │ │ 帧 2: [逻辑 10ms][GC 1ms][逻辑 5.67ms] ← 预算内 GC │ │ 帧 3: [逻辑 10ms][逻辑 6.67ms] ← 预算内无 GC │ │ │ │ 优势GC 在可控时机触发帧时间稳定在 11-12ms │ │ 结果帧率稳定在 83-90 FPS无掉帧 │ └─────────────────────────────────────────────────┘2.2 预算值的设定不同游戏类型有不同的 GC 预算需求// 典型预算值60 FPS 16.67ms/帧 // - 高性能游戏竞技类 ≤ 1KB/帧 → 60KB/秒 // - 中等性能游戏RPG ≤ 4KB/帧 → 240KB/秒 // - 休闲游戏 ≤ 16KB/帧 → 960KB/秒 // - 加载/过场场景 ≤ 1MB/帧 → 一次性 // 30 FPS 场景33.33ms/帧 // - 高性能游戏 ≤ 2KB/帧 → 60KB/秒 // - 中等性能游戏≤ 8KB/帧 → 240KB/秒 // 120 FPS 场景8.33ms/帧 // - 高性能游戏 ≤ 0.5KB/帧 → 60KB/秒 // - 中等性能游戏≤ 2KB/帧 → 240KB/秒预算设定的原则越低越稳定预算越低GC 频率越高但每次 GC 的代价越小。越高越高效预算越高GC 频率越低但每次 GC 的代价越大。找到平衡点通过 Profiler 实测找到帧时间稳定的最大预算值。2.3 GC 预算监控器public class GCBudgetMonitor : MonoBehaviour { [Header(Budget Settings)] [SerializeField] private float _budgetKBPerFrame 4f; // 4KB/帧 [SerializeField] private bool _logViolations true; [SerializeField] private int _alertThreshold 3; // 连续 3 帧超标才报警 private long _lastGCCount; private long _lastHeapSize; private float _budgetBytes; private int _violationCount; void Awake() { _budgetBytes _budgetKBPerFrame * 1024; _lastGCCount GC.CollectionCount(0); _lastHeapSize GC.GetTotalMemory(false); } void Update() { long currentHeap GC.GetTotalMemory(false); long heapDelta currentHeap - _lastHeapSize; // 检查是否超预算 if (heapDelta _budgetBytes) { _violationCount; if (_violationCount _alertThreshold _logViolations) { Debug.LogWarning( $[{Time.frameCount}] GC Budget Violation: ${heapDelta / 1024f:F1}KB allocated $(budget: {_budgetKBPerFrame}KB)); } } else { _violationCount 0; } _lastHeapSize currentHeap; _lastGCCount GC.CollectionCount(0); } // 手动触发 GC在安全时机 public void CollectAtSafeTime() { // 在场景加载、过场动画等安全时机手动触发 GC.Collect(0, GCCollectionMode.Optimized, false); } }2.4 Unity Profiler 预算验证// 使用 ProfilerRecorder 监控 GC 分配 using Unity.Profiling; public class ProfilerGCTracker : MonoBehaviour { private ProfilerRecorder _gcAllocRecorder; private ProfilerRecorder _gcCollectRecorder; private float _threshold 4096f; // 4KB void OnEnable() { _gcAllocRecorder ProfilerRecorder.StartNew( ProfilerCategory.Memory, GC.Alloc, 1); _gcCollectRecorder ProfilerRecorder.StartNew( ProfilerCategory.Memory, GC.Collect, 1); } void OnDisable() { _gcAllocRecorder?.Dispose(); _gcCollectRecorder?.Dispose(); } void Update() { if (_gcAllocRecorder.Valid) { long allocBytes _gcAllocRecorder.LastValue; if (allocBytes _threshold) { Debug.LogWarning( $Frame {Time.frameCount}: GC Alloc {allocBytes} bytes); } } } }2.5 预算设定的实践原则在实际项目中预算值的设定需要结合项目特点和性能目标。以下是预算设定的实践原则从低到高从较低的预算值如 1KB/帧开始逐步增加直到找到帧时间稳定的最大值。考虑目标帧率60 FPS 游戏的预算应低于 30 FPS 游戏因为每帧可用时间更短。区分场景类型加载场景的预算可以远高于运行场景因为加载期允许较长的帧时间。监控 Gen 0 GC 频率Gen 0 GC 的频率是预算是否合理的关键指标。每秒超过 10 次 Gen 0 GC 说明预算过高。三、对象池化策略3.1 通用对象池public class ObjectPoolT where T : class, new() { private readonly StackT _pool; private readonly ActionT? _onRent; private readonly ActionT? _onReturn; private readonly int _maxSize; public ObjectPool(int initialSize, int maxSize 1024, ActionT? onRent null, ActionT? onReturn null) { _pool new StackT(initialSize); _maxSize maxSize; _onRent onRent; _onReturn onReturn; // 预热预创建 initialSize 个对象 for (int i 0; i initialSize; i) { var item new T(); _onReturn?.Invoke(item); // 初始化时调用 Return 回调 _pool.Push(item); } } public T Rent() { T item _pool.Count 0 ? _pool.Pop() : new T(); _onRent?.Invoke(item); // 租用时调用初始化回调 return item; } public void Return(T item) { if (_pool.Count _maxSize) { _onReturn?.Invoke(item); // 归还时调用清理回调 _pool.Push(item); } // 超过最大容量则丢弃让 GC 回收 } public int Count _pool.Count; public int Capacity _pool.Count; // 池中对象数 }3.2 Unity GameObject 池public class GameObjectPool { private readonly StackGameObject _pool; private readonly GameObject _prefab; private readonly Transform _parent; private readonly int _maxSize; public GameObjectPool(GameObject prefab, int initialSize, int maxSize 64, Transform? parent null) { _prefab prefab; _parent parent ?? new GameObject($Pool_{prefab.name}).transform; _maxSize maxSize; _pool new StackGameObject(initialSize); // 预热 for (int i 0; i initialSize; i) { var go Object.Instantiate(prefab, _parent); go.SetActive(false); _pool.Push(go); } } public GameObject Rent(Vector3 position, Quaternion rotation) { GameObject go _pool.Count 0 ? _pool.Pop() : Object.Instantiate(_prefab, _parent); go.transform.SetPositionAndRotation(position, rotation); go.SetActive(true); return go; } public void Return(GameObject go) { if (_pool.Count _maxSize) { go.SetActive(false); go.transform.SetParent(_parent, false); _pool.Push(go); } else { Object.Destroy(go); // 超出容量销毁 } } public void Prewarm(int count) { while (_pool.Count count) { var go Object.Instantiate(_prefab, _parent); go.SetActive(false); _pool.Push(go); } } public int AvailableCount _pool.Count; public int ActiveCount _parent?.childCount ?? 0; }3.3 List 池化// 避免频繁 new ListT 和扩容 public static class ListPoolT { [ThreadStatic] private static StackListT? _pool; private static StackListT Pool _pool ?? new StackListT(); public static ListT Rent() { return Pool.Count 0 ? Pool.Pop() : new ListT(64); } public static void Return(ListT list) { list.Clear(); // 清空但保留内部数组容量 Pool.Push(list); } // 便捷方法租用→操作→归还→返回结果 public static T[] RentProcessAndReturn(FuncListT, T[] process) { var list Rent(); try { return process(list); } finally { Return(list); } } } // 使用 var array ListPoolEnemy.RentProcessAndReturn(list { for (int i 0; i enemies.Count; i) { if (enemies[i].IsAlive) list.Add(enemies[i]); } return list.ToArray(); // 仅一次数组分配 });3.4 池化策略决策对象类型是否池化原因子弹/投射物是高频创建/销毁粒子特效是高频创建/销毁敌人/NPC是战斗场景高频拾取物是中频创建/销毁UI 面板是场景切换频繁场景配置数据否低频创建生命周期长日志/调试对象否低频调试时允许分配3.5 池化策略的权衡对象池化虽然能有效减少 GC 分配但也引入了额外的复杂性。以下是池化策略的权衡分析池化的优势消除分配池化对象在首次创建后不再分配后续租用/归还不产生 GC。内存预分配所有池化对象在初始化时分配运行时无内存波动。性能可预测租用/归还操作的时间复杂度为 O(1)性能可预测。池化的代价初始化开销预热池需要预先创建对象增加初始化时间。内存占用池化对象始终占用内存即使暂时不需要。复杂度需要管理池的生命周期、容量限制、归还逻辑。池化决策原则高频创建/销毁的对象应该池化如子弹、特效。低频创建的对象不需要池化如场景配置。大对象谨慎池化占用 Gen2 空间。小对象不需要池化Gen 0 GC 回收成本低。四、分代回收配合策略4.1 理解分代 GC// CLR 的分代 GC 策略 // Gen0: 短命对象频繁回收~1ms // - 每帧分配的新对象默认进入 Gen0 // - GC.Collect(0) 只回收 Gen0 // Gen1: 中等寿命较少回收~5ms // - Gen0 存活对象晋升到 Gen1 // - 作为 Gen0 和 Gen2 之间的缓冲区 // Gen2: 长命对象很少回收~50ms // - Gen1 存活对象晋升到 Gen2 // - 单例、缓存、场景配置等 // LOH: 大对象(≥85KB)直接进入 Gen2 // - 回收代价极高需要压缩 // 优化原则 // 1. 让短命对象在 Gen0 被回收 → 控制每帧分配量 // 2. 避免对象晋升到 Gen1/Gen2 → 控制引用持有时间 // 3. 大对象避免频繁分配 → LOH 回收代价极高 // 4. 在安全时机手动触发 Gen0 GC → 避免运行时意外触发4.2 分代 GC 配合对象池// 对象池的目的是让对象不进入 GC 回收周期 // 池化的对象始终被强引用不会被 GC 回收 // 但需要注意池化对象不能太大否则占用 Gen2/LOH 空间 // 池化对象的代际管理 // 1. 对象首次创建 → Gen0 // 2. 归还到池 → 仍被池引用停留在当前代 // 3. 池本身是长命对象Gen2→ 池化对象最终晋升到 Gen2 // 4. 池化对象在 Gen2 中不会频繁回收但也无法回收 // 因此池化适合中等大小、高频复用的对象 // 不适合超大对象会占用 Gen2 空间4.3 手动 GC 触发时机public class GCTrigger : MonoBehaviour { private float _gcTimer 0; private const float GC_INTERVAL 5f; // 每 5 秒检查一次 void Update() { _gcTimer Time.deltaTime; // 在 Loading 屏幕、过场动画等安全时机触发 if (_gcTimer GC_INTERVAL IsSafeTime()) { // 仅触发 Gen0 GC开销最小 GC.Collect(0, GCCollectionMode.Optimized, false); _gcTimer 0; } } private bool IsSafeTime() { // 判断当前是否是安全时机 // 1. 不在战斗中 // 2. 不在过场动画 // 3. 帧时间充裕 return Time.deltaTime 0.016f * 0.8f; // 帧时间低于预算 80% } // 场景切换时触发完整 GC public void FullGCOnSceneLoad() { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // 清理所有代释放未使用内存 } }4.4 分代 GC 与内存压力的关系当系统内存压力增大时如 Windows 通知内存不足.NET GC 会自动调整行为以释放内存。这种调整包括降低 Gen 0 预算更频繁地触发 Gen 0 GC。提高压缩阈值更早触发 Gen 2 压缩。回收 LOH回收大对象堆中的死对象。理解这种自动调整机制有助于正确设置 GC 预算——在内存富余时预算可以稍高在内存紧张时预算应该降低。五、场景加载的 GC 控制5.1 场景加载的 GC 特点// 场景加载是 GC 分配的高峰期 // 1. 反序列化 Prefab/ScriptableObject → 大量对象分配 // 2. Instantiate → GameObject Component 分配 // 3. 资源加载 → Texture/Mesh/AudioClip 分配 // 4. 初始化逻辑 → 集合/缓存分配 // 策略将加载期 GC 与运行期 GC 隔离 // 1. 加载期允许大量 GC 分配 // 2. 加载完成后触发完整 GC // 3. 运行期严格控制 GC 预算5.2 分阶段加载public class SceneLoader : MonoBehaviour { public IEnumerator LoadSceneAsync(string sceneName) { // 阶段 1异步加载场景 var op SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); op.allowSceneActivation false; while (op.progress 0.9f) yield return null; // 阶段 2预加载资源 yield return PreloadAssets(); // 阶段 3激活场景 op.allowSceneActivation true; yield return null; // 阶段 4初始化场景对象 yield return InitializeScene(); // 阶段 5触发 GC 清理加载垃圾 yield return new WaitForEndOfFrame(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // 阶段 6预热对象池 PrewarmPools(); } private IEnumerator PreloadAssets() { // 预加载常用资源到缓存 var handles new ListUnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle(); foreach (var path in _preloadPaths) { var handle Addressables.LoadAssetAsyncGameObject(path); handles.Add(handle); } yield return Addressables.LoadAssetAsyncGameObject(_preloadPaths[0]); foreach (var h in handles) _assetCache.Add(h); } private void PrewarmPools() { // 预热对象池避免运行时 Instantiate foreach (var pool in _pools) pool.Prewarm(pool.InitialSize); } }5.3 场景卸载的 GC 控制public IEnumerator UnloadSceneAsync(Scene scene) { // 阶段 1释放资源引用 ReleaseAssetReferences(); // 阶段 2清空对象池 ClearPools(); // 阶段 3异步卸载场景 var op SceneManager.UnloadSceneAsync(scene); while (!op.isDone) yield return null; // 阶段 4触发 GC 清理 yield return null; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // 阶段 5可选 - 释放未使用的资源 yield return Resources.UnloadUnusedAssets(); }5.4 场景加载 GC 控制的工程实践场景加载的 GC 控制在工程实践中需要与项目的加载流程紧密结合。以下是场景加载 GC 控制的工程实践建议加载前 GC在场景加载前触发一次 Gen 0 GC释放上一场景的残留对象。加载后 Full GC场景加载完成后触发 Full GC清理所有代。异步加载使用 Addressables 或 SceneManager 的异步加载 API将 GC 分散到多帧。对象池预热在场景加载完成后预热新场景需要的对象池避免运行时 Instantiate。六、GC 生命周期管理策略6.1 对象生命周期分类// 按生命周期将对象分为四类分别管理 // 1. 永久对象Permanent // - 单例管理器、配置数据、核心系统 // - 生命周期 应用生命周期 // - 策略静态引用或 DontDestroyOnLoad // 2. 场景对象Scene-scoped // - 场景中的 GameObject、场景配置 // - 生命周期 场景生命周期 // - 策略场景加载时分配卸载时释放 // 3. 帧对象Frame-scoped // - 临时计算结果、碰撞检测结果 // - 生命周期 单帧 // - 策略预分配缓冲区栈上分配 // 4. 池化对象Pooled // - 子弹、特效、敌人等频繁创建/销毁的对象 // - 生命周期 可复用 // - 策略对象池管理6.2 生命周期管理框架public class LifetimeManager : MonoBehaviour { // 永久对象注册 private readonly HashSetobject _permanentObjects new(); // 场景对象跟踪 private readonly ListGameObject _sceneObjects new(256); // 帧缓冲区预分配 private readonly Collider[] _frameOverlapBuffer new Collider[64]; private readonly RaycastHit[] _frameRayBuffer new RaycastHit[32]; // 对象池 private readonly DictionaryType, object _pools new(); public T RentPooledT() where T : class, new() { if (!_pools.TryGetValue(typeof(T), out var pool)) { pool new ObjectPoolT(32); _pools[typeof(T)] pool; } return ((ObjectPoolT)pool).Rent(); } public void ReturnPooledT(T item) where T : class, new() { if (_pools.TryGetValue(typeof(T), out var pool)) ((ObjectPoolT)pool).Return(item); } public ref Collider[] FrameOverlapBuffer ref _frameOverlapBuffer; public ref RaycastHit[] FrameRayBuffer ref _frameRayBuffer; public void OnSceneLoaded() { _sceneObjects.Clear(); // 注册新场景对象 } public void OnSceneUnloaded() { // 释放场景对象引用 _sceneObjects.Clear(); // 清空池化对象 foreach (var pool in _pools.Values) { // 清空池让 GC 回收 } _pools.Clear(); // 触发 GC GC.Collect(); } }七、完整 GC 优化策略总结7.1 分层优化策略┌─────────────────────────────────────────────────┐ │ Layer 1: 零分配编程热路径 │ │ - Span/stackalloc │ │ - 预分配缓冲区 │ │ - 值类型替代引用类型 │ │ - 静态委托缓存 │ ├─────────────────────────────────────────────────┤ │ Layer 2: 对象池化中频路径 │ │ - GameObject 池 │ │ - List/数组池 │ │ - StringBuilder 池 │ │ - 事件/委托池 │ ├─────────────────────────────────────────────────┤ │ Layer 3: GC 预算控制全局 │ │ - 帧级 GC 预算监控 │ │ - 安全时机手动 GC │ │ - 分代回收配合 │ ├─────────────────────────────────────────────────┤ │ Layer 4: 场景级 GC 管理宏观 │ │ - 加载/卸载 GC 隔离 │ │ - 分阶段加载 │ │ - 资源预加载与预热 │ └─────────────────────────────────────────────────┘7.2 GC 优化检查清单层级检查项状态L1Update 中无字符串拼接☐L1Update 中无 LINQ☐L1Update 中无闭包/lambda☐L1Physics 使用 NonAlloc☐L1Mesh 数据已缓存☐L1Material 使用 PropertyBlock☐L1Animator 参数使用 Hash☐L2子弹/特效使用对象池☐L2List/数组复用☐L2StringBuilder 池化☐L3GC 预算监控器部署☐L3安全时机 GC 触发☐L3Profiler GC Alloc 4KB/帧☐L4场景加载后 Full GC☐L4对象池预热☐L4资源预加载☐八、实战完整的 GC 管理系统public class GCManager : MonoBehaviour { public static GCManager Instance { get; private set; } null!; [Header(Budget)] [SerializeField] private float _budgetKBPerFrame 4f; [SerializeField] private bool _enableAutoGC true; // 预分配帧缓冲区 public readonly Collider[] OverlapBuffer new Collider[128]; public readonly RaycastHit[] RayBuffer new RaycastHit[64]; public readonly ListEnemy EnemyBuffer new(128); // 对象池 private GameObjectPool? _effectPool; private GameObjectPool? _projectilePool; // GC 监控 private long _lastHeapSize; private float _gcTimer; void Awake() { Instance this; _lastHeapSize GC.GetTotalMemory(false); } void Start() { // 预热对象池 _effectPool new GameObjectPool(_effectPrefab, 32, 128); _projectilePool new GameObjectPool(_projectilePrefab, 64, 256); } void Update() { MonitorGC(); MaybeTriggerGC(); } private void MonitorGC() { long currentHeap GC.GetTotalMemory(false); long delta currentHeap - _lastHeapSize; float deltaKB delta / 1024f; if (deltaKB _budgetKBPerFrame) { Debug.LogWarning( $[{Time.frameCount}] GC alloc {deltaKB:F1}KB $exceeds budget {_budgetKBPerFrame}KB); } _lastHeapSize currentHeap; } private void MaybeTriggerGC() { if (!_enableAutoGC) return; _gcTimer Time.deltaTime; if (_gcTimer 10f Time.deltaTime 0.012f) { GC.Collect(0, GCCollectionMode.Optimized, false); _gcTimer 0; } } // 场景加载时调用 public void OnSceneTransition() { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); _effectPool?.Prewarm(32); _projectilePool?.Prewarm(64); } }九、总结与实践建议9.1 核心要点回顾帧级预算与 GC 生命周期策略是 GC 优化的顶层设计建立帧级 GC 预算每帧分配控制在 4KB 以内。部署 GC 监控器实时检测预算违规。对象池化中频对象子弹/特效/列表复用。预分配帧缓冲区Physics/碰撞检测复用。安全时机手动 GC避免运行时意外触发。场景加载 GC 隔离加载期允许分配完成后 Full GC。分阶段加载预加载→激活→初始化→GC→预热。分层优化L1 零分配→L2 池化→L3 预算→L4 场景管理。9.2 实践建议从监控开始先部署 GC 预算监控器了解项目的 GC 基线。逐层优化从 L1 零分配开始逐步到 L4 场景管理。建立编码规范将高风险 API 清单纳入项目规范。CI 集成在 CI 流程中集成 GC 分配检测。定期审计每 2-4 周进行一次 GC 分配审计。9.3 GC 优化决策树需要优化 GC │ ├─ 第一步建立基线 │ └─ 部署 GC 预算监控器 │ └─ 记录当前 GC Alloc 基线 │ ├─ 第二步L1 零分配 │ ├─ 消除 Update 中的字符串操作 │ ├─ 消除 Update 中的 LINQ │ ├─ 使用 NonAlloc Physics API │ └─ 缓存 Mesh/Material/Animator │ ├─ 第三步L2 池化 │ ├─ 子弹/特效 → GameObject 池 │ ├─ 集合 → ListPool │ └─ 临时缓冲区 → ArrayPool │ ├─ 第四步L3 预算 │ ├─ 调整 GC 预算值 │ ├─ 部署安全时机 GC │ └─ 配合分代回收 │ └─ 第五步L4 场景 ├─ 场景加载 Full GC ├─ 分阶段加载 └─ 对象池预热GC 优化不是一次性工作而是贯穿整个项目生命周期的持续工程。从编码规范到 CI 检测从 Profiler 监控到发布前审计需要建立完整的 GC 管理流程。