如何强制.NET回收并缩减已分配的内存?
.NET中GCHandle释放后内存回收的测试问题
问题背景
我写了一段用GCHandle固定内存(用于和C++互操作持久化)的代码,现在要写测试用例验证:释放数组的GCHandle后,对应的大对象内存能被垃圾回收。
测试时用了一个包含float数组的大对象,通过GC.GetTotalAllocatedBytes(true)或WorkingSet64能看到内存分配量增长,幅度和估算的数组大小一致。但即使不固定数组,移除对象后这些数值也不会下降。我已经试过强制跨代回收:
GC.Collect(); GC.WaitForPendingFinalizers();
但内存占用始终没降低。我知道GC可能会保留内存复用,但想找到办法释放测试用的大块内存。是我用错了指标,还是这根本没法人为控制?
示例代码
using System.Diagnostics; using System.Runtime.InteropServices; namespace TestApp { public class MemUser { private float[] data; private GCHandle dataHandle; public float[] Data { get { return data; } set { data = value; } } public MemUser(int numData) { data = new float[numData]; } public long RoughSize() { return data.Length * sizeof(float); } public void GetPinnedHandles() { if (dataHandle.IsAllocated) dataHandle.Free(); dataHandle = GCHandle.Alloc(dataHandle, GCHandleType.Pinned); } public void FreePinnedHandles() { if (dataHandle.IsAllocated) dataHandle.Free(); } public void Clear() { FreePinnedHandles(); data = null; } } public class Program { public static void Main(string[] args) { const int numFloats = 500 * 1000 * 1000; long beforeCreation = GetProcessMemory(); // 创建占用大块内存的对象 MemUser testMemory = new MemUser(numFloats); long roughMemSize = testMemory.RoughSize(); // 实际场景会固定内存,现在先测试不固定的情况 //testMemory.GetPinnedHandles(); // 确认内存占用上升 long afterCreation = GetProcessMemory(); long difference = afterCreation - beforeCreation; TestResult(difference, roughMemSize); // 销毁对象 testMemory.Clear(); testMemory = null; // 这里测试失败,内存甚至可能上升 long afterDeletion = GetProcessMemory(); difference = afterCreation - afterDeletion; TestResult(difference, roughMemSize); } public static long GetProcessMemory() { Process currentProcess = System.Diagnostics.Process.GetCurrentProcess(); GC.Collect(); GC.WaitForPendingFinalizers(); long totalBytesOfMemoryUsed = GC.GetTotalAllocatedBytes(true); //long totalBytesOfMemoryUsed = currentProcess.WorkingSet64; return totalBytesOfMemoryUsed; } public static void TestResult(long difference, long expected) { if (difference >= expected) Console.Write($"PASS, difference ({difference}) >= {expected}\n"); else Console.Write($"FAIL, difference ({difference}) < {expected}\n"); } } }
问题分析与解决方案
1. 内存指标使用错误
你用的两个指标都不适合用来判断当前托管内存的使用情况:
GC.GetTotalAllocatedBytes(true):这个统计的是进程启动以来累计分配的所有内存字节数,只会增加不会减少,哪怕内存被回收了,这个值也不会下降。它的作用是跟踪内存分配总量,不是当前内存占用。WorkingSet64:这是进程当前占用的物理内存,操作系统会缓存内存页,CLR也会保留回收后的内存作为内存池,避免频繁向系统申请/释放内存的开销。所以即使GC回收了内存,这个值也可能不会立刻下降。
正确的指标:使用GC.GetMemoryInfo().HeapSizeBytes,它返回当前托管堆的总大小,能准确反映当前托管内存的使用情况,回收后会明显下降。
2. GCHandle分配代码存在bug
你的GetPinnedHandles方法里有个致命错误:
dataHandle = GCHandle.Alloc(dataHandle, GCHandleType.Pinned);
这里你把dataHandle本身作为参数传给Alloc,而不是要固定的数组data。正确的写法应该是:
dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
这个错误会导致你根本没固定目标数组,反而创建了一个指向dataHandle的固定句柄,可能引发内存泄漏。
3. 强制回收的正确姿势
单次GC.Collect()可能不够,建议执行两次回收,确保终结器执行后残留的对象也被回收:
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
参数说明:
GC.MaxGeneration:回收所有代的内存GCCollectionMode.Forced:强制触发回收- 最后两个
true:压缩托管堆,确保内存碎片被整理
4. 验证对象回收的可靠方式
如果想准确验证对象是否被回收,可以用WeakReference跟踪:
// 创建对象后生成弱引用 WeakReference weakRef = new WeakReference(testMemory); // 销毁对象 testMemory.Clear(); testMemory = null; // 强制回收 GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); // 检查弱引用是否失效,失效说明对象已被回收 if (!weakRef.IsAlive) { Console.WriteLine("对象已被成功回收"); }
修正后的测试代码示例
using System.Diagnostics; using System.Runtime.InteropServices; namespace TestApp { public class MemUser { private float[] data; private GCHandle dataHandle; public float[] Data { get { return data; } set { data = value; } } public MemUser(int numData) { data = new float[numData]; } public long RoughSize() { return data.Length * sizeof(float); } public void GetPinnedHandles() { if (dataHandle.IsAllocated) dataHandle.Free(); // 修正:固定目标数组data,而不是dataHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); } public void FreePinnedHandles() { if (dataHandle.IsAllocated) dataHandle.Free(); } public void Clear() { FreePinnedHandles(); data = null; } } public class Program { public static void Main(string[] args) { const int numFloats = 500 * 1000 * 1000; // 使用正确的内存指标 long beforeCreation = GC.GetMemoryInfo().HeapSizeBytes; MemUser testMemory = new MemUser(numFloats); long roughMemSize = testMemory.RoughSize(); // 可以打开下面一行测试固定内存的情况 //testMemory.GetPinnedHandles(); long afterCreation = GC.GetMemoryInfo().HeapSizeBytes; long difference = afterCreation - beforeCreation; TestResult(difference, roughMemSize, "内存分配验证"); // 创建弱引用跟踪对象 WeakReference weakRef = new WeakReference(testMemory); // 销毁对象 testMemory.Clear(); testMemory = null; // 强制完整回收 GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); // 检查对象是否回收 if (!weakRef.IsAlive) Console.WriteLine("对象已被成功回收"); else Console.WriteLine("对象未被回收"); long afterDeletion = GC.GetMemoryInfo().HeapSizeBytes; difference = afterCreation - afterDeletion; TestResult(difference, roughMemSize * 0.8, "内存回收验证"); // 允许少量误差,因为CLR可能保留少量内存 } public static void TestResult(long difference, long expected, string testName) { if (difference >= expected) Console.Write($"{testName} PASS, difference ({difference}) >= {expected}\n"); else Console.Write($"{testName} FAIL, difference ({difference}) < {expected}\n"); } } }
内容的提问来源于stack exchange,提问作者mike
相关产品推荐
相关产品推荐

