使用非托管内存时出现奇怪的内存使用情况

Strange memory usage when using unmanaged memory

我们的应用程序需要通过互操作调用某个非托管的第 3 方库。该应用程序线程密集,每分钟调用此函数数千次,这就是我们不泄漏内存至关重要的原因。

当 运行 我们服务器上的应用程序时,我注意到应用程序的内存占用量在增加,经过数周的导航问题,我最终将其缩小到这个非常奇怪的问题:

我在使用非托管代码时观察到一个非常奇怪的行为,在分配和取消分配非托管指针之后,内存似乎没有按预期减少。

这是重现问题的最少代码:

Platform: x64
Build: Release (optimised)
Windows Server 2012
64GB memory
Dual Xeon processors with 64 logical cores
<gcServer enabled="true />

和代码

class Program
{
    static void Main()
    {
        // LargeFile.abc is representing a byte[] of ~300kb
        byte[] largeArray = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "Data", "LargeFile.abc"));
        TestMemoryAllocation(largeArray);

        Console.WriteLine("Done");
        Console.ReadLine();
    }

    private static void TestMemoryAllocation(byte[] byteArray)
    {
        Parallel.For(0,
            1000000,
            i =>
            {
                FromStream(byteArray, ptr =>
                {
                    // simulate some work with the unmanaged pointer
                    Thread.Sleep(50);
                });
            });
    }

    private static void FromStream(byte[] src, Action<IntPtr> func)
    {
        IntPtr unmanagedArray = IntPtr.Zero;
        try
        {
            // allocate the memory on the unmanaged heap
            unmanagedArray = Marshal.AllocHGlobal(src.Length);

            // do something with this unmanaged pointer
            func(unmanagedArray);
        }
        finally
        {
            // free the space
            Marshal.FreeHGlobal(unmanagedArray);
        }
    }
}

通常情况下,我的预期是根据核心数量和线程调度,内存会上下波动但最终会稳定并徘徊在最小值和最大值之间。相反,这是我观察到的:

私有工作字节在 ProcessExplorer/TaskManager(这是一个 Windows 服务器)中不断增加,内存分析器(使用 ANTS 和 JetBrains dotMemory)都报告运行时内存消耗增加.

这是 运行 应用程序完成时 dotMemory 的屏幕截图:

红色线是实际发生的情况(当运行上面的代码示例时),而蓝色线是我期望的样子。

我也试过 adding/removing GC 压力 (GC.Add/RemoveMemoryPressure) 并没有帮助,但这是可以理解的,因为这里没有发生 GC。我在 Window 10 桌面上尝试了同样的事情,但观察到相同的行为。

这是怎么回事?我们的服务器按进程控制内存使用,如果内存超过一定水平,它会终止并重新启动进程,这意味着我们必须小心我们的内存使用并且需要能够控制它。

更新:使用 Thread.Sleep(0) 而不是 Thread.Sleep(50) 的图形:

非常感谢,

好吧,对你的测试代码做一些内存分析(请注意,我将分配增加到 100 倍以更快地到达那里:):

  1. GC 从未发生过。这并不奇怪,因为你并没有真正分配太多托管内存——一分钟后(当我遇到内存不足崩溃时),堆已经增长了大约 10 kiB,没有理由让 GC 启动。
  2. 我得到 40 个工作线程(在 4 核 CPU 上)。这也不足为奇,但可能是您遇到问题的原因。当您使用的同步代码没有完成足够的 CPU 工作时,工作线程池将增加线程池以适应更多的并发工作负载。它不关心你使用了多少内存。这意味着,如果您的线程完成速度不快于新线程的启动速度,那么您的内存使用量将不断增加,直到进程崩溃。

随着内存的增加,检查您的服务器上 运行 有多少个线程。使用量的跳跃似乎很好地跟随了新工作线程的创建。请注意,代码中只有 Thread.Sleep(50),这限制了您获得的最大线程数;我预计您的实际工作量可能远不止于此。

因此,在我看来,这并不是真正的内存泄漏问题 - 这是一个节流问题。您可以通过一个系统来限制您允许用于此特定问题的线程数来解决此问题。在您的测试应用程序中,这很简单:

Parallel.For(0, 1000000, new ParallelOptions { MaxDegreeOfParallelism = 10 },
  i =>
  {
    FromStream(byteArray, ptr =>
    {
       // simulate some work with the unmanaged pointer
       Thread.Sleep(50);
    });
  }
);

瞧 - 您的内存使用量达到峰值并保持稳定。调整并行度以适应您实际可以并行执行的工作量以及可用内存量。