【问题标题】:Strange memory usage when using unmanaged memory使用非托管内存时奇怪的内存使用情况
【发布时间】:2019-07-17 06:59:25
【问题描述】:

我们的应用程序需要通过互操作调用某个非托管的第 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 运行应用程序直到完成时的屏幕截图:

红线是实际发生的情况(在运行上述代码示例时),而蓝线是我预期它的样子。

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

发生了什么事?我们的服务器通过进程控制内存使用,如果内存超过一定水平,它会杀死并重新启动进程,这意味着我们必须小心我们的内存使用并需要能够控制这一点。

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

非常感谢,

【问题讨论】:

    标签: c# unmanaged


    【解决方案1】:

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

    1. 没有发生 GC。这并不奇怪,因为您并没有真正分配太多托管内存 - 一分钟后(当我遇到内存不足崩溃时),堆增长了大约 10 kiB,GC 没有理由启动。
    2. 我得到 四十个工作线程(在 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);
        });
      }
    );
    

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

    【讨论】:

    • 非常感谢Luaan的分析。前段时间我们确实测试了线程的数量,它在服务器上并没有增加到超过 200 个,其中 32 个被命名为用于此特定调用的线程。我会再看一遍,并放入每隔几分钟打勾的性能计数器,以提供线程统计信息。对于我们的实际程序,我们使用了来自 RX.Observables 的 32 个事件循环调度器,这些调度器实际上是调用这个 3rd 方系统的那些。
    • 除非 RX 中存在错误(当然可能),否则我看不出有超过 32 个线程可以进入,但一切皆有可能,但我认为您是正确的,因此接受了答案。这个问题随着限制线程的数量而消失了,所以一定是这样。再次感谢您。
    • @kha 当然,您的生产代码中可能存在一些与我在示例代码中发现的问题无关的问题;我肯定会建议添加一些性能计数器来跟踪您正在进行的分配和解除分配。如果它们匹配(并且在任何时候分配的内存数量与您在任务管理器中看到的没有太大差异),那么您可能会遇到内存碎片问题,例如。此外,如果您在生产代码中使用固定字节数组(通过MemoryStream),您可能会遇到严重的托管堆碎片。
    • @kha 您可以使用 CLRProfiler 轻松查看它们 - 只需在显示问题时对进程进行完整的内存转储,然后查看 CLRProfiler 中的堆 - 如果您看到大部分内存未使用,您遇到了碎片问题; Marc 使用ArrayPool&lt;byte&gt; 的建议将使您的烦恼在这种情况下消失。
    猜你喜欢
    • 2020-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-10
    • 2015-02-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多