2011-04-14 29 views
4

我最近發現一個非常奇怪的(對我來說)從C#使用的IEnumString COM對象的內存泄漏。具體而言,使用已包含先前調用產生的值的字符串數組調用IEnumString.Next方法會導致內存泄漏。ref string []內存泄漏與NET/COM互操作

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(下一頁)方法這樣引起的泄漏,這是由重複運行了很長一段時間,看到「專用字節」驗證櫃檯不斷上漲。

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); 

所以它似乎是項數組必須是空的編組層正確地釋放複製到它未管理的字符串。即使如此,數組將返回相同的值,即具有非空數組不會導致返回錯誤的字符串值,但它只是泄漏了一些。

+0

小心合成測試,這種運行相同代碼片斷的種類數百萬次。使用Perfmon.exe並觀察測試過程的垃圾收集數量。 COM使用非託管內存,它不會給GC帶來很大的壓力。 – 2011-04-14 12:16:00

+1

這不是一個綜合測試,我沒有運行只顯示的代碼片段,而是運行它的整個程序段。(這是一個小的測試應用程序,用於檢索服務器應用程序的命名空間(分支和葉子),並簡單地打印他們出來,這是一遍又一遍地完成的。如果運行第一個代碼片段,進程很快就會消耗數百MB的內存,而第二個則會穩定在20MB左右)。 – 2011-04-14 13:45:16

+0

我也使用Process Explorer檢查了GC堆內存的使用情況,在這兩種情況下都沒有消耗太多內存,所以我非常有把握地確信它是泄露的未受管理的COM內存。 – 2011-04-14 13:49:17

回答

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; 
    } 
} 
+0

是的,你當然是對的!我沒有考慮到編組層會將已存在於項目(rgelt)數組中的託管字符串轉換爲需要釋放的非託管COM內存。雖然錯誤不在IEnumString實現對象中,但rgelt參數明確定義爲[out]參數,所以它不應該爲它釋放任何內存。只是這個「out-info」在.NET端丟失了,所以當調用者錯誤時沒有任何警告或錯誤。猜測最好總是使用包裝器進行COM interops,即使它們看起來像「真正的」NET公民! – 2011-04-27 06:48:53