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

如何在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);
            }
        }
    }        
}        

问题分析与解决

一、原代码未出现内存泄漏的原因

  1. 托管内存分支无实际逻辑:else分支完全是空的,调用托管模式时根本没有分配需要泄漏的内存。要实现托管内存泄漏,必须把创建的对象保留在GC无法回收的地方,比如静态集合。
  2. 非托管内存尝试1无效:unsafe块里的var a = new IntPtr(&buff);只是获取了托管数组buff的引用地址,但buff是局部变量,方法执行完毕后会被GC正常回收,这里既没泄漏托管内存,也没涉及非托管内存泄漏。
  3. 非托管内存尝试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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 11:45:55