让我们用我们手头的所有工具来调查这个问题。
首先,让我们看看这些对象是什么,为了做到这一点,我将给定的代码放在 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;
}
...
所以我们开始了,缓存在所有请求之间共享,并且在所有请求完成时不会释放。