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

Marshal.FreeHGlobal内存释放原理及C#内存残留问题咨询

关于C#非托管内存释放后进程内存残留的问题解答

一、为什么释放后进程内存仍有1-2MB增长?

这个现象其实是Windows内存管理机制和CLR托管内存模型共同作用的结果,核心原因有这几点:

  • 非托管内存的回收特性:你用Marshal.AllocHGlobal分配的是非托管内存,调用FreeHGlobal释放后,操作系统并不会立刻把这部分内存归还给系统,而是将其保留在当前进程的内存池中,作为后续非托管内存分配的备用空间——这么做是为了避免频繁向系统申请/释放内存带来的性能开销。
  • 托管对象未被GC回收:List<clsA>里存储的是clsA的托管实例,虽然你调用free()释放了非托管内存,但这些clsA对象本身还留在托管堆中。直到CLR的垃圾回收器(GC)触发回收,才会释放这些托管对象占用的内存。而你的代码里List<clsA>的引用一直存在到Main方法后期,GC可能还没来得及处理这部分内存。
  • 内存统计的延迟性:任务管理器显示的内存占用是进程的工作集大小(Working Set),包含当前正在使用的物理内存。操作系统需要时间调整工作集大小,不会在内存释放后立刻更新统计数据。

二、如何释放剩余内存?

如果你确实需要尽可能回收这部分内存,可以尝试以下方法,但要注意这些方法都有适用场景,不要滥用:

  1. 触发强制GC回收
    在释放非托管内存后,先让clsA实例失去引用,再手动触发GC(这只是建议GC执行回收,无法保证立刻完成):
    foreach (var a in l) {
        a.free();
    }
    // 清除引用,让GC能识别到可回收对象
    l.Clear();
    l = null;
    // 触发GC并等待回收完成
    GC.Collect();
    GC.WaitForPendingFinalizers();
    
  2. 提示操作系统回收物理内存
    通过Windows API调整进程工作集大小,让操作系统尽可能回收未使用的物理内存(注意这不会减少虚拟内存占用,只是调整物理内存分配):
    [System.Runtime.InteropServices.DllImport("kernel32.dll")]
    private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);
    
    // 在GC回收后调用
    SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
    
  3. 采用规范的资源释放模式
    让clsA实现IDisposable接口,用using语句自动管理资源,既能保证非托管内存及时释放,也能让GC更快识别需要回收的对象:
    class clsA : IDisposable {
        private IntPtr _buffer;
        private bool _disposed = false;
    
        public clsA() {
            _buffer = Marshal.AllocHGlobal(1024);
        }
    
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool disposing) {
            if (_disposed) return;
            if (_buffer != IntPtr.Zero) {
                Marshal.FreeHGlobal(_buffer);
                _buffer = IntPtr.Zero;
            }
            _disposed = true;
        }
    
        ~clsA() {
            Dispose(false);
        }
    }
    
    // 使用示例
    static void Main(string[] args) {
        List<clsA> l = new List<clsA>();
        for(int i = 0; i < 8192; i++) {
            l.Add(new clsA());
        }
        Console.WriteLine("Added");
        foreach (var a in l) {
            a.Dispose();
        }
        l.Clear();
        l = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.ReadLine();
    }
    

三、Marshal.FreeHGlobal是如何释放内存的?

Marshal.FreeHGlobal本质是对Windows系统API LocalFree的封装,工作流程如下:

  • 它接收的指针必须是通过Marshal.AllocHGlobal分配的,这个指针指向**Windows全局堆(Global Heap)**中的内存块。
  • 调用LocalFree后,操作系统会把该内存块标记为“可用”,但不会立刻将内存归还给系统,而是保留在进程的全局堆池中,供后续AllocHGlobal调用时直接复用——这是为了提升内存分配的性能。
  • 注意:FreeHGlobal只能释放AllocHGlobal分配的内存,不能用于AllocCoTaskMem的内存(后者需要用Marshal.FreeCoTaskMem,它封装的是CoTaskMemFree,对应COM任务堆的内存管理)。

内容的提问来源于stack exchange,提问作者Harsh Shah

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 09:04:45