如何在C#中让Linux内核回收进程未使用的内存?
在Linux中实现类似Windows的内存强制回收效果的方法
问题背景
以下代码会创建2GiB的对象,执行垃圾回收后进程仍占用2GiB以上内存:
- 在Windows系统中,调用
kernel32.dll的SetProcessWorkingSetSize()可强制系统回收内存,效果如下:
- 在Linux(Ubuntu 20.04)系统中,调用libc的
madvise并传入MADV_DONTNEED和MADV_PAGEOUT参数均返回-1且错误码为0;调用malloc_trim(0)执行成功,但进程工作集仍占用2GiB以上内存,效果如下:
问题:如何在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
相关产品推荐
相关产品推荐

