GCHandle.Alloc会分配内存吗?

12

我正在使用SciTech提供的.NET Memory Profiler来降低程序的内存分配速率和减少垃圾回收的频率。

但是出乎意料的是,根据分析器的结果,最大的分配量似乎来自于我进行的GCHandle.Alloc调用,以将现有的.NET数组映射到本机的OpenGL中。

我的理解是,调用GCHandle.Alloc不会分配内存,它只是锁定托管堆上的现有内存?

那么,我是错了还是分析器是错了呢?


1
它可能试图告诉你什么阻止托管对象被垃圾回收。是的,通常会使用GCHandle来保持传递给OpenGL的纹理或网格等对象的生命。如果这些是固定句柄,那就有点糟糕了,但这只是你使用的任何OpenGL包装库的一个方面。 - Hans Passant
分析器甚至为我分配的每个GCHandle分配了特定的内存量 - 8字节。而且,似乎每次使用GCHandle.Alloc时托管堆都会增长8字节。因此,它似乎实际上确实在托管堆上分配空间,尽管我不知道用于什么? - kaalus
CLR 会维护一个单独的句柄表用来存储 GCHandles。是的,8 字节听起来大概是正确的。如果这个句柄表无限增长,那么可能是因为在不再需要资源时未调用所需的 GCHandle.Free()。 - Hans Passant
@HansPassant 它不会无限增长,我正在释放句柄。问题在于它会导致托管堆增长并最终触发垃圾回收。这就是我试图最小化的问题。 - kaalus
你确定需要这么频繁地固定内存吗?任何形式的内存固定都会因为自然原因对垃圾回收器造成压力。 - Peter Ritchie
@PeterRitchie 如何在不固定内存的情况下传递指向非托管库(如OpenGL)的指针? - kaalus
3个回答

12

.NET参考源代码可供任何人查看,您可以查看并自行了解。

如果您深入研究 GCHandle.Alloc,您会发现它调用了一个名为InternalAlloc的本地方法:

[System.Security.SecurityCritical]  // auto-generated
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[ResourceExposure(ResourceScope.None)]
internal static extern IntPtr InternalAlloc(Object value, GCHandleType type);

深入挖掘CLR代码,你会看到对MarshalNative::InternalAlloc的内部调用,最终会调用到:

hnd = GetAppDomain()->CreateTypedHandle(objRef, type);

这将调用ObjectHandle::CreateTypedHandle -> HandleTable::HndCreateHandle -> HandleTableCache->TableAllocSingleHandleFromCache,如果句柄不存在于缓存中,则会分配该句柄。

正如@Antosha纠正我的,调用的地方并不是通过ComDelegate(这其实没有多大意义),而是通过MarshalNative。确实会发生一次分配,但不是在托管堆上,而是运行时为管理指向GC对象的句柄根而保留的外部堆上。唯一在托管堆上发生的分配是保存指向表中地址的指针的IntPtr。尽管如此,完成后仍应确保调用GCHandle.Free


1
这不正确。GCHandle.Alloc 调用的是 MarshalNative::GCHandleInternalAlloc,而不是 COMDelegate::InternalAlloc。前者在由 GC 引擎维护的句柄表中创建一个新记录并返回其地址。该句柄表不是托管堆的一部分,这意味着 GCHandle.Alloc 的调用对托管堆大小没有影响。 - Antosha
1
@Antosha 谢谢你纠正我。重新阅读我的回答也让我明白,对于 GCHandle 来调用 ComDelegate 没有多少意义,而且我对运行时的调查是错误的。我已经更正了我的回答。 - Yuval Itzchakov
1
感谢您纠正答案,我现在同意了。当一个GCHandle存储在托管堆上(例如作为对象字段或GCHandle[]的元素),它会消耗与IntPtr相同的内存,即在32位架构上为4字节,在64位架构上为8字节。当您调用GCHandle.Alloc时,句柄变为已分配状态,这还会额外消耗IntPtr.Size的非托管运行时内存,直到您调用GCHandle.Free来释放句柄。 - Antosha
@YuvalItzchakov,我以前从未听说过.NET参考源,谢谢你分享。关于你所说的“深入CLR代码”的部分,你是否还使用其他参考资料来查看InternalAlloc的作用?当我点击它时,它没有任何反应。 - Dave
@Dave,它不接受你的操作,因为它是本地代码。你需要查看GitHub上的CoreCLR存储库(https://github.com/dotnet/coreclr)。 - Yuval Itzchakov

1

分析器甚至为我分配的每个GCHandle分配了特定的内存量 - 8字节。而且似乎每次GCHandle.Alloc时托管堆都会增长8字节。所以它似乎确实在托管堆上分配了空间,尽管我不知道为什么?

我不知道一个句柄怎么可能更小 :) 我做了一些测试:

Console.WriteLine("Is 64 bit: {0}, IntPtr.Size: {1}", Environment.Is64BitProcess, IntPtr.Size);

int[][] objects = new int[100000][];
for (int i = 0; i < objects.Length; i++)
{
    objects[i] = new int[] { 0 };
}

long w1 = Environment.WorkingSet;

GCHandle[] handles = new GCHandle[objects.Length];

for (int i = 0; i < handles.Length; i++)
{
    //handles[i] = new GCHandle(handles);
    //handles[i] = GCHandle.Alloc(objects[i]);
    handles[i] = GCHandle.Alloc(objects[i], GCHandleType.Pinned);
}

Console.WriteLine("Allocated");
long w2 = Environment.WorkingSet;
Console.WriteLine("Used: {0}, by handle: {1}", w2 - w1, ((double)(w2 - w1)) / handles.Length);
Console.ReadKey();

这是一个小程序。如果你运行它,你会发现一个“空”的GCHandle(使用new GCHandle()创建的)占用IntPtr.Size内存。如果您使用ILSpy查看它,这一点是很清楚的:它有一个单独的IntPtr字段。如果您固定某些对象,则它将占用2 * IntPtr.Size内存。这可能是因为它必须在CLR表中写入一些东西(大小为IntPtr),以及其内部的IntPtr。
引自https://dev59.com/tnXYa4cB1Zd3GeqP6G17#18122621 “它使用CLR内置的专用GC句柄表。您可以使用GCHandle.Alloc()为此表分配一个条目,并稍后使用GCHandle.Free()释放它。垃圾收集器只需将此表中的条目添加到它自己发现的对象图中即可进行收集。”

0

来自msdn

一个新的GCHandle,用于保护对象免受垃圾回收。当不再需要此GCHandle时,必须使用Free释放它。

因此,即使没有进行实际分配,我想在调用Free之前也不能释放预分配的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接