【问题标题】:ref string[] memory leak with NET/COM interopNET/COM 互操作的 ref string[] 内存泄漏
【发布时间】:2011-08-05 10:47:10
【问题描述】:

我最近发现了一个非常奇怪的(对我来说)内存泄漏,用于 C# 中使用的 IEnumString COM 对象。具体来说,使用一个字符串数组调用 IEnumString.Next 方法,该字符串数组已经包含先前调用产生的值会导致内存泄漏。

IEnumString 在 C# 端看起来像这样:

    [InterfaceType(1)]
[Guid("00000101-0000-0000-C000-000000000046")]
public interface IEnumString
{
    void Clone(out IEnumString ppenum);
    void RemoteNext(int celt, string[] rgelt, out int pceltFetched);
    void Reset();
    void Skip(int celt);
}

像这样调用RemoteNext(Next)方法会导致泄漏,通过长时间重复运行并看到“Private Bytes”计数器不断上升来验证。

string[] item = new string[100]; // OBS! Will be re-used for each call!
for (; ; )
{
    int fetched;
    enumString.RemoteNext(item.Length, item, out fetched);
    if (fetched > 0)
    {
        for (int i = 0; i < fetched; ++i)
        {
            // do something with item[i]
        }
    }
    else
    {
        break;
    }
}

但是为每个调用创建一个新的字符串 item[] 数组以某种方式使泄漏消失了。

for (; ; )
{
    int fetched;
    string[] item = new string[100]; // Create a new instance for each call.
    enumString.RemoteNext(item.Length, item, out fetched);
    if (fetched > 0)
    {
        for (int i = 0; i < fetched; ++i)
        {
            // do something with item[i]
        }
    }
    else
    {
        break;
    }
}

在第一种情况下出了什么问题?我猜发生的情况是为 IEnumString.Next 的 rgelt 参数分配的 COM 内存,应该由调用者释放,不知何故不是。

但第二种情况却很奇怪。

编辑:有关一些附加信息,这是 RemoteNext 方法在 ILDASM 和 .NET Reflector 中的“实现”。

.method public hidebysig newslot abstract virtual 
        instance void  RemoteNext([in] int32 celt,
                                  [in][out] string[]  marshal( lpwstr[ + 0]) rgelt,
                                  [out] int32& pceltFetched) runtime managed internalcall
{
} // end of method IEnumString::RemoteNext



[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime)]
void RemoteNext(
[In] int celt, 
[In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPWStr, SizeParamIndex=0)] string[] rgelt, 
out int pceltFetched);

编辑 2:您还可以通过在调用 RemoteNext 之前将字符串值添加到字符串数组(仅包含空值)来使泄漏出现在第二种非泄漏情况中。

string[] item = new string[100]; // Create a new instance for each call.
item[0] = "some string value"; // THIS WILL CAUSE A LEAK
enumString.RemoteNext(item.Length, item, out fetched);

所以看起来项目数组必须为空,编组层才能正确释放复制到它的非托管字符串。即便如此,数组将返回相同的值,即具有非空数组不会导致返回错误的字符串值,它只是泄漏一些。

【问题讨论】:

  • 注意综合测试,这种测试会运行相同的代码 sn-p 数百万次。使用 Perfmon.exe 并观察测试进程的垃圾回收次数。 COM 使用非托管内存,不会对 GC 造成太大压力。
  • 这不是综合测试,我不仅运行了显示的代码 sn-p,而是运行了它所使用的整个程序段。(这是一个检索命名空间(分支和叶) 并简单地将它们打印出来。这是一遍又一遍地完成。如果使用第一个代码 sn-p 运行该进程将很快消耗数百 MB 内存,而第二个它将稳定在大约20MB)。
  • 我还用 Process Explorer 检查了 GC 堆内存的使用情况,在这两种情况下它都没有消耗太多内存,所以我很确定泄漏的是非托管 COM 内存。跨度>

标签: c# .net memory-leaks interop


【解决方案1】:

IEnumString - 它只是一个接口。底层的 COM 对象是什么?这是主要嫌疑人。

IEnumString的非托管声明:

HRESULT Next(
  [in]   ULONG celt,
  [out]  LPOLESTR *rgelt,
  [out]  ULONG *pceltFetched
);

如您所见,第二个参数 rgelt 只是一个指向字符串数组的指针 - 没什么特别的,但是当您进行托管调用时

string[] item = new string[100]; // Create a new instance for each call.
item[0] = "some string value"; // THIS WILL CAUSE A LEAK
enumString.RemoteNext(item.Length, item, out fetched);

您在item[0] 中的字符串似乎已转换为未正确释放的 LPOLESTR。因此试试这个:

string[] item = new string[1];
for (; ; )
{
    int fetched;
    item[0] = null;
    enumString.RemoteNext(1, item, out fetched);
    if (fetched == 1)
    {
       // do something with item[0]
    }
    else
    {
        break;
    }
}

【讨论】:

  • 是的,你当然是对的!我不认为编组层会将项目 (rgelt) 数组中已经存在的托管字符串转换为需要释放的非托管 COM 内存。错误不在于 IEnumString 实现对象,但 rgelt 参数明确定义为 [out] 参数,因此它不应该为它释放任何内存。只是这个“输出信息”在 .NET 端丢失了,所以当调用者出错时没有警告或错误。猜猜最好总是为 COM 互操作使用包装器,即使它们看起来像“真正的”NET 公民!
猜你喜欢
  • 1970-01-01
  • 2012-05-20
  • 2020-06-24
  • 2012-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-22
相关资源
最近更新 更多