c# - HttpClient 导致 mscorlib 中的 Node<Object> 泄漏

标签 c# memory memory-leaks httpclient mscorlib

考虑以下程序,with all of HttpRequestMessage, and HttpResponseMessage, and HttpClient disposed properly.收集后,它最后总是以大约 50MB 的内存结束。请求数加零,未回收的内存增加一倍。

   class Program
    {
        static void Main(string[] args)
        {
            var client = new HttpClient { 
                   BaseAddress = new Uri("http://localhost:5000/")};

            var t = Task.Run(async () =>
            {
                var resps = new List<Task<HttpResponseMessage>>();
                var postProcessing = new List<Task>();

                for (int i = 0; i < 10000; i++)
                {
                    Console.WriteLine("Firing..");
                    var req = new HttpRequestMessage(HttpMethod.Get,
                                                        "test/delay/5");
                    var tsk = client.SendAsync(req);
                    resps.Add(tsk);
                    postProcessing.Add(tsk.ContinueWith(async ts =>
                    {
                        req.Dispose();
                        var resp = ts.Result;
                        var content = await resp.Content.ReadAsStringAsync();
                        resp.Dispose();
                        Console.WriteLine(content);
                    }));
                }

                await Task.WhenAll(resps);
                resps.Clear();
                Console.WriteLine("All requests done.");
                await Task.WhenAll(postProcessing);
                postProcessing.Clear();
                Console.WriteLine("All postprocessing done.");
            });

            t.Wait();
            Console.Clear();

            var t2 = Task.Run(async () =>
            {
                var resps = new List<Task<HttpResponseMessage>>();
                var postProcessing = new List<Task>();

                for (int i = 0; i < 10000; i++)
                {
                    Console.WriteLine("Firing..");
                    var req = new HttpRequestMessage(HttpMethod.Get,
                                                        "test/delay/5");
                    var tsk = client.SendAsync(req);
                    resps.Add(tsk);
                    postProcessing.Add(tsk.ContinueWith(async ts =>
                    {
                        var resp = ts.Result;
                        var content = await resp.Content.ReadAsStringAsync();
                        Console.WriteLine(content);
                    }));
                }

                await Task.WhenAll(resps);
                resps.Clear();
                Console.WriteLine("All requests done.");
                await Task.WhenAll(postProcessing);
                postProcessing.Clear();
                Console.WriteLine("All postprocessing done.");
            });

            t2.Wait();
            Console.Clear();
            client.Dispose();

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

在使用内存分析器进行快速调查后,占用内存的对象似乎都是 Node<Object> 类型。在 mscorlib 中。

我最初的想法是,它是一些内部字典或堆栈,因为它们是使用 Node 作为内部结构的类型,但 我无法为通用 Node<T> 找到任何结果 在引用源中,因为这实际上是 Node<object>输入。

这是一个错误,还是某种预期的优化(我不会认为始终保留的按比例消耗内存是一种优化)?纯学术,什么是Node<Object> .

任何有助于理解这一点的帮助将不胜感激。谢谢:)

更新:为了推断更大的测试集的结果,我通过节流对其进行了轻微优化。

这是更改后的程序。现在,it seems to stay consistent at 60-70MB ,对于 100 万个请求集。我仍然对那些 Node<object> 感到困惑确实如此,并且允许维护如此大量的不可回收对象。

从这两个结果的差异得出的逻辑结论让我猜测,这可能不是 HttpClient 或 WebRequest 的问题,而是直接 Root 于异步的问题 - 因为这两个测试中的真正变体是数字在给定时间点存在的未完成的异步任务。 这只是快速检查的推测。

static void Main(string[] args)
{

    Console.WriteLine("Ready to start.");
    Console.ReadLine();

    var client = new HttpClient { BaseAddress = 
                    new Uri("http://localhost:5000/") };

    var t = Task.Run(async () =>
    {
        var resps = new List<Task<HttpResponseMessage>>();
        var postProcessing = new List<Task>();

        for (int i = 0; i < 1000000; i++)
        {
            //Console.WriteLine("Firing..");
            var req = new HttpRequestMessage(HttpMethod.Get, "test/delay/5");
            var tsk = client.SendAsync(req);
            resps.Add(tsk);
            var n = i;
            postProcessing.Add(tsk.ContinueWith(async ts =>
            {
                var resp = ts.Result;
                var content = await resp.Content.ReadAsStringAsync();
                if (n%1000 == 0)
                {
                    Console.WriteLine("Requests processed: " + n);
                }

                //Console.WriteLine(content);
            }));

            if (n%20000 == 0)
            {
                await Task.WhenAll(resps);
                resps.Clear();
            }

        }

        await Task.WhenAll(resps);
        resps.Clear();
        Console.WriteLine("All requests done.");
        await Task.WhenAll(postProcessing);
        postProcessing.Clear();
        Console.WriteLine("All postprocessing done.");
    });

    t.Wait();
    Console.Clear();
    client.Dispose();

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

最佳答案

让我们用我们手头的所有工具来调查这个问题。

首先,让我们看看这些对象是什么,为了做到这一点,我将给定的代码放在 Visual Studio 中并创建了一个简单的控制台应用程序。我并排在 Node.js 上运行一个简单的 HTTP 服务器来处理请求。

将客户端运行到最后并开始将 WinDBG 附加到它,我检查托管堆并获得以下结果:

0:037> !dumpheap
Address       MT     Size
02471000 00779700       10 Free
0247100c 72482744       84     
...
Statistics:
      MT    Count    TotalSize Class Name
...
72450e88      847        13552 System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]]
...

!dumpheap 命令将托管堆中的所有对象转储到那里。这可能包括应该被释放的对象(但还没有,因为 GC 还没有开始)。在我们的例子中,这应该很少见,因为我们只是在打印输出之前调用了 GC.Collect() 并且在打印输出之后不应该运行其他任何东西。

值得注意的是上面的特定行。那应该是您在问题中所指的 Node 对象。

接下来,让我们看看该类型的单个对象,我们获取该对象的 MT 值,然后像这样再次调用 !dumpheap,这将只过滤掉我们感兴趣的对象。

0:037> !dumpheap -mt 72450e88   
 Address       MT     Size
025b9234 72450e88       16     
025b93dc 72450e88       16     
...

现在在列表中随机抓取一个,然后通过调用 !gcroot 命令询问调试器为什么该对象仍在堆上:

0:037> !gcroot 025bbc8c
Thread 6f24:
    0650f13c 79752354 System.Net.TimerThread.ThreadProc()
        edi:  (interior)
            ->  034734c8 System.Object[]
            ->  024915ec System.PinnableBufferCache
            ->  02491750 System.Collections.Concurrent.ConcurrentStack`1[[System.Object, mscorlib]]
            ->  09c2145c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]]
            ->  09c2144c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]]
            ->  025bbc8c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]]

Found 1 unique roots (run '!GCRoot -all' to see all roots).

现在很明显,我们有一个缓存,并且该缓存维护一个堆栈,堆栈实现为链表。如果我们进一步思考,我们将在引用源中看到该列表是如何使用的。为此,让我们首先检查缓存对象本身,使用 !DumpObj

0:037> !DumpObj 024915ec 
Name:        System.PinnableBufferCache
MethodTable: 797c2b44
EEClass:     795e5bc4
Size:        52(0x34) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
724825fc  40004f6        4        System.String  0 instance 024914a0 m_CacheName
7248c170  40004f7        8 ...bject, mscorlib]]  0 instance 0249162c m_factory
71fe994c  40004f8        c ...bject, mscorlib]]  0 instance 02491750 m_FreeList
71fed558  40004f9       10 ...bject, mscorlib]]  0 instance 025b93b8 m_NotGen2
72484544  40004fa       14         System.Int32  1 instance        0 m_gen1CountAtLastRestock
72484544  40004fb       18         System.Int32  1 instance 605289781 m_msecNoUseBeyondFreeListSinceThisTime
7248fc58  40004fc       2c       System.Boolean  1 instance        0 m_moreThanFreeListNeeded
72484544  40004fd       1c         System.Int32  1 instance      244 m_buffersUnderManagement
72484544  40004fe       20         System.Int32  1 instance      128 m_restockSize
7248fc58  40004ff       2d       System.Boolean  1 instance        1 m_trimmingExperimentInProgress
72484544  4000500       24         System.Int32  1 instance        0 m_minBufferCount
72484544  4000501       28         System.Int32  1 instance        0 m_numAllocCalls

现在我们看到了一些有趣的东西,堆栈实际上被用作缓存的空闲列表。源代码告诉我们如何使用空闲列表,特别是在如下所示的 Free() 方法中:

http://referencesource.microsoft.com/#mscorlib/parent/parent/parent/parent/InternalApis/NDP_Common/inc/PinnableBufferCache.cs

/// <summary>
/// Return a buffer back to the buffer manager.
/// </summary>
[System.Security.SecuritySafeCritical]
internal void Free(object buffer)
{
  ...
  m_FreeList.Push(buffer);
}

就是这样,当调用者处理完缓冲区后,它会返回缓存,缓存然后将其放入空闲列表中,然后将空闲列表用于分配目的

[System.Security.SecuritySafeCritical]
internal object Allocate()
{
  // Fast path, get it from our Gen2 aged m_FreeList.  
  object returnBuffer;
  if (!m_FreeList.TryPop(out returnBuffer))
    Restock(out returnBuffer);
  ...
}

最后但同样重要的是,让我们了解为什么当我们完成所有这些 HTTP 请求后缓存本身没有被释放?这就是为什么。通过在 mscorlib.dll!System.Collections.Concurrent.ConcurrentStack.Push() 上添加断点,我们看到以下调用堆栈(嗯,这可能只是缓存用例之一,但这是有代表性的)

mscorlib.dll!System.Collections.Concurrent.ConcurrentStack<object>.Push(object item)
System.dll!System.PinnableBufferCache.Free(object buffer)
System.dll!System.Net.HttpWebRequest.FreeWriteBuffer()
System.dll!System.Net.ConnectStream.WriteHeadersCallback(System.IAsyncResult ar)
System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken)
System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken)
System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken)
System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped)
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP)

在 WriteHeadersCallback 中,我们完成了标题的写入,因此我们将缓冲区返回到缓存中。此时缓冲区被推回空闲列表,因此我们分配了一个新的堆栈节点。需要注意的关键是缓存对象是 HttpWebRequest 的静态成员。

http://referencesource.microsoft.com/#System/net/System/Net/HttpWebRequest.cs

...
private static PinnableBufferCache _WriteBufferCache = new PinnableBufferCache("System.Net.HttpWebRequest", CachedWriteBufferSize);
...
// Return the buffer to the pinnable cache if it came from there.   
internal void FreeWriteBuffer()
{
  if (_WriteBufferFromPinnableCache)
  {
    _WriteBufferCache.FreeBuffer(_WriteBuffer);
    _WriteBufferFromPinnableCache = false;
  }
  _WriteBufferLength = 0;
  _WriteBuffer = null;
}
...

所以我们开始了,缓存在所有请求之间共享,并且在所有请求完成时不会释放。

关于c# - HttpClient 导致 mscorlib 中的 Node<Object> 泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27561131/

相关文章:

c# - 保护/省略 NHibernate 中域实体的选定属性(子类、投影、?)

c# - 使用 IEnumerable 重载参数函数

asp.net - ASP.NET 应用程序的物理内存太多?

Calloc() 正在分配先前分配的内存

firefox - 如何测量 Firefox 附加内存使用情况

c++ - 内存泄漏, vector push_back c++

memory-leaks - 使用复杂模型结构时何时调用 Backbone.Relational.store.unregister()

c# - 如何将 MvcHtmlString 呈现为 HTML

C# 函数参数数组与列表

C++ 析构函数和内存分配,以及未定义的行为