考虑以下我们可以在 C# 中定义的最简单 COM 对象的示例(使用带有 .NET Framework 4.0 的 Visual Studio 2010 SP1 构建):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace CcwTestLib
{
[ComVisible(true)]
[Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]
public class CcwTestObject
{
}
}
我们编译此程序集并使用 regasm(或 Visual Studio 中的内置选项)将其注册为 COM 互操作。
现在,我们只需用 C++ 编写一个非托管 Win32 控制台应用程序,该应用程序除了创建该对象的实例并释放它 100,000 次之外,什么也不做。例如,使用以下程序:
#include "stdafx.h"
// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject =
{ 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
IUnknown *pTestObject = NULL;
const int iCount = 100000;
wprintf(L"Allocating COM instance %i times...\n", iCount);
for (int i = 0; i < iCount; i++)
{
HRESULT hr = CoCreateInstance(CLSID_CcwTestObject,
NULL,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(LPVOID*)&pTestObject);
if (FAILED(hr))
{
wprintf(L"Error: %i", hr);
return -1;
}
pTestObject->Release();
}
CoUninitialize();
return 0;
}
在本地系统上运行此应用程序时,它在大约 820 毫秒内完成,消耗大约 32MB 内存。将 iCount
增加到 10,000,000 会使程序需要更长的时间才能完成(当然),但查看内存消耗,它会增加到大约 92MB,并在程序执行的剩余时间内保持该状态。到目前为止没有什么太奇怪的。
现在是有趣的部分,引出了我的问题。让我们从 .NET 类中删除 Guid
属性(并禁用自动 COM 注册(如果已启用),以便以前的注册在注册表中仍然完好无损)并重建程序集。
我们再次运行测试程序,并将 iCount
设置为 100,000。这次程序大约在90,000ms内完成!这比以前慢了大约100 倍!
更有趣和麻烦的是当我们将 iCount 增加到 10,000,000 并启动程序时。如果我们使用 Process Explorer 或 VMMap 或类似程序监控其内存消耗,我们可以看到它慢慢增加,但它不会像我们预期的那样停留在 92MB。相反,它似乎会永远持续下去。据推测,当用完大约 2GB 的虚拟内存空间(因为它是 x86 进程)时,应用程序将会崩溃,但由于它的运行速度如此之慢,我们没有等到这种情况发生,而是在 1,200MB 左右退出。
应该注意的是,使用 COM 对象、调用其方法等等(如果我们定义了任何方法)都可以正常工作,因为创建对象的所有必要信息都存储在注册表中。我们系统上的一部分如下所示:
[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll
其中 CLSID 正确指向程序集及其代码库,以及程序集中的显式类型。
我们还发现,将属性中的 Guid 更改为注册的 Guid 以外的任何值都会产生同样的问题。
那么为什么会发生这种情况呢?这是 .NET 中的错误吗?这个问题有什么解决办法吗?
我很高兴能够深入了解这个问题,我们花了大约一周的时间才从我们产品中发现的内存泄漏中缩小到这个更加简化的场景。
Consider the following example of the simplest COM object we can define in C# (built using Visual Studio 2010 SP1 with .NET framework 4.0):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace CcwTestLib
{
[ComVisible(true)]
[Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]
public class CcwTestObject
{
}
}
We compile this assembly and register it for COM interop using regasm (or the built in option in Visual Studio).
Now we simply write an unmanaged Win32 console application in C++ that does nothing other than to create an instance of this object and release it 100,000 times. For example using the following program:
#include "stdafx.h"
// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject =
{ 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
IUnknown *pTestObject = NULL;
const int iCount = 100000;
wprintf(L"Allocating COM instance %i times...\n", iCount);
for (int i = 0; i < iCount; i++)
{
HRESULT hr = CoCreateInstance(CLSID_CcwTestObject,
NULL,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(LPVOID*)&pTestObject);
if (FAILED(hr))
{
wprintf(L"Error: %i", hr);
return -1;
}
pTestObject->Release();
}
CoUninitialize();
return 0;
}
When running this application on our local system it completed in about 820ms and consumes about 32MB memory. Increasing iCount
to 10,000,000 makes the program take a lot longer to complete (of course) but looking at the memory consumption it increases to about 92MB and stays there for the remainder of the execution of the program. Nothing too strange so far.
Now for the interesting part, leading up to my question. Let's remove the Guid
attribute from the .NET class (and disable automatic COM registration if enabled so that the previous registration is still left intact in the registry) and rebuild the assembly.
We run the test program again with iCount
set to 100,000. This time the program completes in about 90,000ms! That is about 100 times slower than before!
Even more interesting and troublesome is when we increase iCount
to 10,000,000 and start the program. If we monitor its memory consumption using Process Explorer or VMMap or a similar program we can see it slowly increase, but it doesn’t stop at 92MB as we might expect. Instead it seems to continue on forever. Presumably the application will crash when running out of virtual memory space at around 2GB (since it is an x86 process), but since it’s moving so slow we didn’t wait for that to happen in this test but quit around 1,200MB.
It should be noted that using the COM object, calling its methods and so on (if we had defined any) works just fine, as it should since all the necessary information for creating the object is stored in the registry. A part of it on our system looks like the following:
[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll
Where the CLSID correctly points to the assembly and its codebase, and the explicit type within the assembly.
We also discovered that altering the Guid in the attribute to anything other than the one it is registered with creates the very same problem.
So why is this happening? Is this a bug in .NET? And is there any workaround to this problem?
I would be very happy for some insight into this problem, which took us about a week to narrow down to this much simplified scenario from a discovered memory leak in our product.
发布评论
评论(1)
看来内存泄漏是一个错误,已在 .NET 4.5 中修复。
有关更多详细信息,请参阅问题 Microsoft Connect。
引用他们的回答:
It seems the memory leak was a bug that has been fixed in .NET 4.5.
For more details see the issue at Microsoft Connect.
To quote from their answer: