2017-02-28 41 views
4

我有一個作爲Windows服務運行的繼承.NET 4.0應用程序。我不是任何.NET專家,但是在寫了30多年的代碼之後,我知道如何找到自己的方式。.NET4 ExpandoObject使用漏洞內存

服務第一次啓動時,它的時鐘大約在70MB的私人工作集。服務運行的時間越長,佔用的內存就越多。這種增長並非如你所看到的那麼戲劇化,而是我們已經看到在應用程序運行很長時間(100天以上)後達到多GB(5GB是當前記錄)的情況。我將ANTS Memory Profiler附加到正在運行的實例,並發現ExpandoObject的使用似乎佔用了幾兆字節的字符串,而這些字符串不會被GC清理。可能還有其他泄漏,但這是最明顯的,所以首先受到攻擊。

我從其他SO帖子瞭解到,讀取(但不寫)動態分配的屬性時,ExpandoObject的「正常」使用會生成內部RuntimeBinderException。

dynamic foo = new ExpandoObject(); 
var s; 
foo.NewProp = "bar"; // no exception 
s = foo.NewProp;  // RuntimeBinderException, but handled by .NET, s now == "bar" 

您可以看到VisualStudio中發生異常,但最終在.NET內部處理中發現異常,並且您只需返回所需的值即可。

除了...異常的Message屬性中的字符串似乎停留在堆上,並且永遠不會收到垃圾收集,即使在生成它的ExpandoObject超出範圍之後。

簡單的例子:

using System; 
using System.Dynamic; 

namespace ConsoleApplication2 
{ 
    class Program 
    { 
     public static string foocall() 
     { 
     string str = "", str2 = "", str3 = ""; 
     object bar = new ExpandoObject(); 
     dynamic foo = bar; 
     foo.SomePropName = "a test value"; 
     // each of the following references to SomePropName causes a RuntimeBinderException - caught and handled by .NET 
     // Attach an ANTS Memory profiler here and look at string instances 
     Console.Write("step 1?"); 
     var s2 = Console.ReadLine(); 
     str = foo.SomePropName; 
     // Take another snapshot here and you'll see an instance of the string: 
     // 'System.Dynamic.ExpandoObject' does not contain a definition for 'SomePropName' 
     Console.Write("step 2?"); 
     s2 = Console.ReadLine(); 
     str2 = foo.SomePropName; 
     // Take another snapshot here and you'll see 2nd instance of the identical string 
     Console.Write("step 3?"); 
     s2 = Console.ReadLine(); 
     str3 = foo.SomePropName; 

     return str; 
     } 
     static void Main(string[] args) 
     { 
     var s = foocall(); 
     Console.Write("Post call, pre-GC prompt?"); 
     var s2 = Console.ReadLine(); 
     // At this point, ANTS Memory Profiler shows 3 identical strings in memory 
     // generated by the RuntimeBinderExceptions in foocall. Even though the variable 
     // that caused them is no longer in scope the strings are still present. 

     // Force a GC, just for S&G 
     GC.Collect(); 
     GC.WaitForPendingFinalizers(); 
     GC.Collect(); 
     Console.Write("Post GC prompt?"); 
     s2 = Console.ReadLine(); 
     // Look again in ANTS. Strings still there. 
     Console.WriteLine("foocall=" + s); 
     } 
    } 
} 

「錯誤」 是在旁觀者的眼睛,我想(我的眼睛說的bug)。我錯過了什麼嗎?這是否正常,並且是由組中的.NET大師預期的?有什麼辦法可以告訴它清除所有的東西嗎?首先不使用動態/ ExpandoObject是最好的方法嗎?

+0

? –

+0

是的,但它只顯示異常字符串的3個實例,無論我發射多少個線程。 – AngryPrimate

回答

4

這似乎是由於編譯器生成的代碼爲動態屬性訪問執行的緩存。 (分析使用VS2015和.NET 4.6的輸出執行;其他編譯器版本可能產生不同的輸出。)

編譯器將此調用寫入類似的內容(根據dotPeek;請注意<>o__0等是令牌這是不合法的C#但是C#編譯器創建):

if (Program.<>o__0.<>p__2 == null) 
{ 
    Program.<>o__0.<>p__2 = CallSite<Func<CallSite, object, string>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof (string), typeof (Program))); 
} 
Func<CallSite, object, string> target1 = Program.<>o__0.<>p__2.Target; 
CallSite<Func<CallSite, object, string>> p2 = Program.<>o__0.<>p__2; 
if (Program.<>o__0.<>p__1 == null) 
{ 
    Program.<>o__0.<>p__1 = CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, "SomePropName", typeof (Program), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[1] 
    { 
     CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null) 
    })); 
} 
object obj3 = Program.<>o__0.<>p__1.Target((CallSite) Program.<>o__0.<>p__1, obj1); 
string str1 = target1((CallSite) p2, obj3); 

Program.<>o__0.<>p__1CallSite<Func<CallSite,object,object>>類型的靜態字段(在私人嵌套類)。它包含對第一次訪問foo.SomePropName時按需編譯的動態方法的引用。 (據推測這是因爲在創建綁定是緩慢的,所以緩存它提供了隨後的訪問一個顯著的速度增加。)

DynamicMethod保持到DynamicILGenerator的參考,其引用DynamicScope最終持有令牌的列表。其中一個令牌是動態生成的字符串'System.Dynamic.ExpandoObject' does not contain a definition for 'SomePropName'。這個字符串存在於內存中,這樣動態生成的代碼就可以拋出(並捕獲)帶有「正確」消息的RuntimeBinderException

總的來說,<>p__1字段保持大約2K的數據存活(包括該字符串的172個字節)。沒有支持的方式來釋放這些數據,因爲它的根源是編譯器生成類型的靜態字段。(你當然可以使用反射到靜態字段設置爲null,但這種做法是非常依賴於當前的編譯器的實現細節,而且很可能在將來打破。)

從我所看到的到目前爲止,似乎使用dynamic在C#代碼中爲每個屬性訪問分配大約2K內存;你可能不得不考慮使用動態代碼的價格。但是(至少在這個簡化的例子中),這個內存只在代碼執行的時候才被分配,所以在程序運行的時間越長,它就不應該繼續使用更多的內存。可能會有不同的泄漏,將工作集推到5GB。 (有三個字符串實例,因爲有三行代碼執行foo.SomePropName;但是,如果您調用foocall 100次,仍然只有三個實例。)

爲了提高性能並減少內存使用量,您可能要考慮使用Dictionary<string, string>Dictionary<string, object>作爲一個簡單的鍵/值存儲(如果可能的話用代碼編寫的方式)。需要注意的是ExpandoObject實現IDictionary<string, object>所以下面的小改寫產生相同的輸出,但避免了動態代碼的開銷:如果放置在使用Task.Factory.StartNew它自己的線程foocall是泄漏仍然存在

public static string foocall() 
{ 
    string str = "", str2 = "", str3 = ""; 

    // use IDictionary instead of dynamic to access properties by name 
    IDictionary<string, object> foo = new ExpandoObject(); 

    foo["SomePropName"] = "a test value"; 
    Console.Write("step 1?"); 
    var s2 = Console.ReadLine(); 

    // have to explicitly cast the result here instead of having the compiler do it for you (with dynamic) 
    str = (string) foo["SomePropName"]; 

    Console.Write("step 2?"); 
    s2 = Console.ReadLine(); 
    str2 = (string) foo["SomePropName"]; 
    Console.Write("step 3?"); 
    s2 = Console.ReadLine(); 
    str3 = (string) foo["SomePropName"]; 

    return str; 
} 
+0

聖莫里。 +10的細節! – AngryPrimate

+0

@AngryPrimate請注意或接受答案,如果有幫助的話! –

+0

所以我想,如果我看到的00或這些字符串掛我身邊的千必須有沒有得到妥善處理一些對象?我認爲,深入研究是沿着這些線條(簡單地說):嘗試訪問,捕獲異常,交易和返回值。我見過的帖子(在這裏和其他地方),也有報道泄密做'的IDictionary <字符串,對象>富=新ExpandoObject()時;',但我沒有挖那裏。我成功地將ExpandoObject的大部分用法改爲Dictionary,但是有一些地方很不直截了當。 – AngryPrimate