如何在C#及.NET 7中制造内存泄漏?现有代码未生效求排查
内存泄漏测试代码未生效及调试器崩溃问题
我正在学习内存转储读取技术,目的是提升内存泄漏排查能力,因此尝试编写不同类型的内存泄漏代码,但遇到了困难。我编写了如下简单API:
public class LeakMemoryController : ControllerBase { public const int MEGABYTE = 1048576; /// <summary> /// /// </summary> /// <param name="managedMemory"></param> /// <param name="BigBlock"></param> /// <param name="amountMB"></param> /// <returns></returns> [HttpGet("/leak")] public async Task<MemoryReport> Leak(string managedOrUnamanagedMemory, string BigOrSmallBlock, int amountMB) { var rdn = Random.Shared; int buffSize; if (BigOrSmallBlock.ToLower() == "big") { buffSize = MEGABYTE / 2; } else { //10kb buffSize = 10240; } var iterations = (amountMB * MEGABYTE / buffSize) + 1; if (managedOrUnamanagedMemory.ToLower() == "unmanaged") { for (int c = 0; c < iterations; c++) { byte[] buff = new byte[buffSize]; for (int i = 0; i < buff.Length; i++) { buff[i] = (byte)rdn.Next(0, 255); } unsafe { //Try number 1 var a = new IntPtr(&buff); } //try number 2 Marshal.AllocHGlobal(buffSize); } } else { for (int c = 0; c < iterations; c++) { //todo: work later } } return await ReportMemory(); } [HttpGet("/reportMemory")] public async Task<MemoryReport> ReportMemory() { var process = Process.GetCurrentProcess(); process.Refresh(); MemoryReport report = new MemoryReport(); report.ProcessId = process.Id; report.NonpagedSystemMemorySize64 = process.NonpagedSystemMemorySize64; report.PagedSystemMemorySize64 = process.PagedSystemMemorySize64; report.PagedMemorySize64 = process.PagedMemorySize64; report.PrivateMemorySize64 = process.PrivateMemorySize64; report.VirtualMemorySize64 = process.VirtualMemorySize64 / MEGABYTE; report.WorkingSet64 = process.WorkingSet64 / MEGABYTE; process.Dispose(); return report; } } public class MemoryReport { public long NonpagedSystemMemorySize64 { get; set; } public long PagedSystemMemorySize64 { get; internal set; } public long PagedMemorySize64 { get; internal set; } public long PrivateMemorySize64 { get; internal set; } public long VirtualMemorySize64 { get; internal set; } public long WorkingSet64 { get; internal set; } public int ProcessId { get; internal set; } }
但无论调用多少次Leak接口,内存报告始终维持在60-75MB左右,并未出现预期的内存泄漏。请问我哪里出错了?
编辑1:后续尝试
如下代码会导致调试器崩溃:
if (managedOrUnamanagedMemory.ToLower() == "unmanaged") { for (int c = 0; c < iterations; c++) { IntPtr ptr = Marshal.AllocHGlobal(buffSize); unsafe { var span = new Span<byte>(&ptr, buffSize); for (int i = 0; i < span.Length; i++) { span[i] = (byte)rdn.Next(0, 255); } } } }
问题分析与解决
一、原代码未出现内存泄漏的原因
- 托管内存分支无实际逻辑:
else分支完全是空的,调用托管模式时根本没有分配需要泄漏的内存。要实现托管内存泄漏,必须把创建的对象保留在GC无法回收的地方,比如静态集合。 - 非托管内存尝试1无效:
unsafe块里的var a = new IntPtr(&buff);只是获取了托管数组buff的引用地址,但buff是局部变量,方法执行完毕后会被GC正常回收,这里既没泄漏托管内存,也没涉及非托管内存泄漏。 - 非托管内存尝试2的指标误区:
Marshal.AllocHGlobal(buffSize)确实分配了非托管内存且未释放,但你可能看了错误的内存指标。WorkingSet64是进程实际占用的物理内存,系统会把闲置的非托管内存交换到页文件,所以这个值不会持续增长;而PrivateMemorySize64才是进程私有的内存总量(包括非托管堆),这个指标应该会随调用次数增加而上升。另外,如果amountMB设置过小,内存增长可能不明显。
二、后续尝试代码崩溃的原因
你创建Span的方式完全错误:new Span<byte>(&ptr, buffSize)里的&ptr取的是指针变量本身在栈上的地址,而不是Marshal.AllocHGlobal分配的非托管内存地址。ptr是栈上的局部变量,你用它的地址创建一个大Span并写入数据,会直接覆盖栈上的其他数据,导致栈溢出或内存结构破坏,最终引发调试器崩溃。
正确的写法是直接使用ptr指向的非托管内存地址:
unsafe { var span = new Span<byte>(ptr.ToPointer(), buffSize); Random.Shared.NextBytes(span); // 用内置方法填充随机字节,比循环高效 }
这样操作的是分配的非托管内存,不会破坏栈数据,同时因为没有调用Marshal.FreeHGlobal(ptr),这些内存会持续泄漏,内存占用会随调用次数明显上升。
完整修改示例
要同时实现托管和非托管内存泄漏,代码可以改成这样:
public class LeakMemoryController : ControllerBase { public const int MEGABYTE = 1048576; // 静态集合,用来持有托管对象,防止GC回收 private static readonly List<byte[]> _leakedManagedBuffers = new(); [HttpGet("/leak")] public async Task<MemoryReport> Leak(string managedOrUnamanagedMemory, string BigOrSmallBlock, int amountMB) { var rdn = Random.Shared; int buffSize = BigOrSmallBlock.ToLower() == "big" ? MEGABYTE / 2 : 10240; var iterations = (amountMB * MEGABYTE / buffSize) + 1; if (managedOrUnamanagedMemory.ToLower() == "unmanaged") { for (int c = 0; c < iterations; c++) { IntPtr ptr = Marshal.AllocHGlobal(buffSize); unsafe { var span = new Span<byte>(ptr.ToPointer(), buffSize); rdn.NextBytes(span); } // 不调用Marshal.FreeHGlobal,内存持续泄漏 } } else { for (int c = 0; c < iterations; c++) { byte[] buff = new byte[buffSize]; rdn.NextBytes(buff); _leakedManagedBuffers.Add(buff); // 添加到静态集合,阻止GC回收 } } return await ReportMemory(); } // ReportMemory方法简化优化 [HttpGet("/reportMemory")] public async Task<MemoryReport> ReportMemory() { using var process = Process.GetCurrentProcess(); process.Refresh(); return new MemoryReport { ProcessId = process.Id, NonpagedSystemMemorySize64 = process.NonpagedSystemMemorySize64, PagedSystemMemorySize64 = process.PagedSystemMemorySize64, PagedMemorySize64 = process.PagedMemorySize64, PrivateMemorySize64 = process.PrivateMemorySize64, VirtualMemorySize64 = process.VirtualMemorySize64 / MEGABYTE, WorkingSet64 = process.WorkingSet64 / MEGABYTE }; } } public class MemoryReport { public long NonpagedSystemMemorySize64 { get; set; } public long PagedSystemMemorySize64 { get; internal set; } public long PagedMemorySize64 { get; internal set; } public long PrivateMemorySize64 { get; internal set; } public long VirtualMemorySize64 { get; internal set; } public long WorkingSet64 { get; internal set; } public int ProcessId { get; internal set; } }
测试建议
- 调用接口时设置较大的
amountMB(比如100),多次调用后观察PrivateMemorySize64的变化,这个指标能准确反映内存泄漏情况。 - 托管模式下,静态集合
_leakedManagedBuffers会持续增长,GC无法回收这些数组,托管堆内存会持续上升。 - 非托管模式下,每次分配的内存都未释放,非托管堆内存会持续上升。
内容的提问来源于stack exchange,提问作者Leonardo
相关产品推荐
相关产品推荐

