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

如何在C#中让Linux内核回收进程未使用的内存?

在Linux中实现类似Windows的内存强制回收效果的方法

问题背景

以下代码会创建2GiB的对象,执行垃圾回收后进程仍占用2GiB以上内存:

  • 在Windows系统中,调用kernel32.dll的SetProcessWorkingSetSize()可强制系统回收内存,效果如下:
    Windows内存回收效果
  • 在Linux(Ubuntu 20.04)系统中,调用libc的madvise并传入MADV_DONTNEED和MADV_PAGEOUT参数均返回-1且错误码为0;调用malloc_trim(0)执行成功,但进程工作集仍占用2GiB以上内存,效果如下:
    Linux内存回收效果

问题:如何在Linux系统中实现与Windows相同的内存回收效果?若无法实现,最优策略是什么?

测试代码

using System.Diagnostics;
using System.Runtime;
using System.Runtime.InteropServices;

namespace GCtest
{
    internal class Program
    {
        public static async Task Main()
        {
            PrintMemoryUsage();

            UseLotsOfMemory();

            MemoryUtility.ForceGcAndSetProcessWorkingSet();

            PrintMemoryUsage();

            Console.ReadLine();
        }

        private static void UseLotsOfMemory()
        {
            const int objectSize = 1024*1024*100; // 100 MiB
            const int numberOfObjectsToCreate = 20;
            var datas = new List<byte[]>();
            var rng = new Random(Guid.NewGuid().GetHashCode());

            for (int i = 0; i < numberOfObjectsToCreate; i++)
            {
                byte[] data = new byte[objectSize];
                rng.NextBytes(data);
                datas.Add(data);
                PrintMemoryUsage();
            }
        }

        private static long GetMemoryUsage()
        {
            var process = Process.GetCurrentProcess();
            return process.WorkingSet64;
        }

        private static void PrintMemoryUsage()
        {
            Console.WriteLine();
            Console.WriteLine($"Process working set: {GetMemoryUsage():n0} bytes");
            Console.WriteLine($"GC total memory    : {GC.GetTotalMemory(false):n0} bytes");
        }
    }

    public static class MemoryUtility
    {
        public static void ForceGcAndSetProcessWorkingSet()
        {
            Console.WriteLine("Forcing blocking GC collection and compacting of gen2 LOH and updating OS process working set size...");
            var sw = Stopwatch.StartNew();
            GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
            GC.Collect(generation: 2, GCCollectionMode.Forced, blocking: true, compacting: true);

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                WindowsMemoryUtility.ReleaseUnusedProcessWorkingSetMemory();
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                LinuxMemoryUtility.ReleaseUnusedProcessWorkingSetMemoryWithMallocTrim();
                LinuxMemoryUtility.ReleaseUnusedProcessWorkingSetMemoryWithMadvise_MADV_DONTNEED();
                LinuxMemoryUtility.ReleaseUnusedProcessWorkingSetMemoryWithMadvise_MADV_PAGEOUT();
            }

            Console.WriteLine($"Completed GC and setting process working set in {sw.Elapsed.TotalMilliseconds} ms");
        }

        public class LinuxMemoryUtility
        {
            [DllImport("libc", SetLastError = true)]
            private static extern int madvise(IntPtr addr, UIntPtr length, int advice);

            private const int MADV_DONTNEED = 6;
            private const int MADV_PAGEOUT = 21;

            public static void ReleaseUnusedProcessWorkingSetMemoryWithMadvise_MADV_DONTNEED()
            {
                try
                {
                    var startMemoryAddress = Process.GetCurrentProcess().MainModule.BaseAddress;
                    var memoryLength = new UIntPtr((ulong)Process.GetCurrentProcess().WorkingSet64);

                    Console.WriteLine($"Calling madvise with start: {startMemoryAddress} and length: {memoryLength}");

                    int result = madvise(startMemoryAddress, memoryLength, MADV_DONTNEED);
                    Console.WriteLine($"Result: {result}");

                    if (result != 0)
                    {
                        Console.WriteLine($"madvise errno: {Marshal.GetLastSystemError()}");
                    }
                }
                catch (Exception exc)
                {
                    Console.WriteLine(exc);
                }
            }

            public static void ReleaseUnusedProcessWorkingSetMemoryWithMadvise_MADV_PAGEOUT()
            {
                try
                {
                    var startMemoryAddress = Process.GetCurrentProcess().MainModule.BaseAddress;
                    var memoryLength = new UIntPtr((ulong)Process.GetCurrentProcess().WorkingSet64);

                    Console.WriteLine($"Calling madvise with start: {startMemoryAddress} and length: {memoryLength}");

                    int result = madvise(startMemoryAddress, memoryLength, MADV_PAGEOUT);
                    Console.WriteLine($"Result: {result}");

                    if (result != 0)
                    {
                        Console.WriteLine($"madvise errno: {Marshal.GetLastSystemError()}");
                    }
                }
                catch (Exception exc)
                {
                    Console.WriteLine(exc);
                }
            }

            [DllImport("libc", SetLastError = true)]
            private static extern int malloc_trim(uint pad);

            public static void ReleaseUnusedProcessWorkingSetMemoryWithMallocTrim()
            {
                try
                {
                    Console.WriteLine($"Calling malloc_trim(0)");
                    int result = malloc_trim(0);
                    Console.WriteLine($"Result: {result}");

                    if (result != 1)
                    {
                        Console.WriteLine($"malloc_trim errno: {Marshal.GetLastSystemError()}");
                    }
                }
                catch (Exception exc)
                {
                    Console.WriteLine(exc);
                }
            }
        }

        public class WindowsMemoryUtility
        {
            [DllImport("kernel32.dll", SetLastError = true)]
            static extern bool SetProcessWorkingSetSize(IntPtr proc, int minSize, int maxSize);

            public static void ReleaseUnusedProcessWorkingSetMemory()
            {
                SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle, -1, -1);
            }
        }
    }
}

解决方案

1. 现有代码的问题

当前调用madvise的方式无效:直接传入进程基地址和工作集大小是错误的,madvise需要操作连续的、已分配的虚拟内存区域,而进程地址空间包含代码段、共享库等不可回收部分,并非整块可操作的连续区域。

2. 针对.NET的正确内存回收方法

方法一:使用.NET内置API触发内存归还

完成强制GC后,通过TryStartNoGCRegion+EndNoGCRegion的组合,可触发运行时向系统归还闲置内存:

// 在GC.Collect之后添加
GC.Collect(2, GCCollectionMode.Forced, true, true);
// 申请小范围无GC区域后立即退出,触发内存归还
if (GC.TryStartNoGCRegion(1024 * 1024)) // 申请1MiB区域,可按需调整
{
    GC.EndNoGCRegion();
}

方法二:针对托管堆段调用madvise

通过GC.GetGCMemoryInfo()获取托管堆的具体分段信息,针对这些有效区域调用madvise:

// 需在LinuxMemoryUtility中添加此方法
public static void ReleaseUnusedProcessWorkingSetMemoryWithTargetedMadvise()
{
    try
    {
        var memoryInfo = GC.GetGCMemoryInfo();
        foreach (var segment in memoryInfo.Segments)
        {
            Console.WriteLine($"Calling madvise on segment: {segment.Start}, length: {segment.Length}");
            int result = madvise(segment.Start, new UIntPtr((ulong)segment.Length), MADV_DONTNEED);
            Console.WriteLine($"Segment result: {result}");
        }
    }
    catch (Exception exc)
    {
        Console.WriteLine(exc);
    }
}

注意:MADV_DONTNEED会标记内存页为不需要,系统会在需要时重新分配,可能导致后续内存访问的性能开销;MADV_PAGEOUT仅在特定内核配置下可用,且需要足够权限。

方法三:调整.NET运行时内存配置

通过环境变量或代码配置,让运行时更积极地归还内存:

  • 设置环境变量:export DOTNET_GC_HEAPALLOCATIONSPINWAIT=0(降低内存分配自旋等待,促进GC回收)
  • 代码配置:
GCSettings.LatencyMode = GCLatencyMode.Batch;
GC.AddMemoryPressure(0); // 触发内存压力评估

3. 无法完全复刻Windows效果的原因

Linux与Windows内存管理模型差异极大:

  • Windows的SetProcessWorkingSetSize是强制将闲置内存页换出到分页文件,而Linux的内存回收依赖内核页缓存机制和OOM策略,用户态无法直接强制回收所有闲置页。
  • Linux进程的RSS(工作集)包含共享库、页缓存等系统级资源,这些部分无法通过用户态调用直接回收。

4. 最优策略

若无法实现完全一致的效果,建议采用以下策略:

  • 优先依赖.NET内置GC机制,避免频繁手动强制回收(手动GC会影响性能)。
  • 仅在明确需要释放内存的场景(如长时间闲置前)执行强制GC+内存归还操作。
  • 启用Linux环境默认的Server GC模式,提升大内存场景下的GC效率。
  • 若必须降低RSS,结合madvise(MADV_DONTNEED)针对托管堆段操作,并接受后续可能的性能开销。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 19:17:01