You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何强制.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 06:37:06