2017-02-28 4 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); 
     } 
    } 
} 

"バグ" はbeholdersの目にある、私は考えます(私の目には、バグを言います)。何か不足していますか?これは正常であり、グループの.NETマスターが期待していますか?物事をクリアするためにそれを伝える方法はありますか?最初に動的/ ExpandoObjectを使用しないことをお勧めしますか?

+0

まだ存在漏れているのですか? –

+0

はい、ただし、どのスレッドが何回発火したかにかかわらず、例外文字列の3つのインスタンスしか表示されません。 – AngryPrimate

答えて

4

これは、ダイナミックプロパティアクセス用のコンパイラ生成コードによって実行されたキャッシングによるものと思われます。

str = foo.SomePropName;の呼び出しは、コンパイラによって次のように書き換えられます(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__1はタイプCallSite<Func<CallSite,object,object>>のプライベートネストされたクラスの静的フィールド()です。 foo.SomePropNameが初めてアクセスされたときにオンデマンドでコンパイルされる動的メソッドへの参照を保持します。 (バインディングを作成するので、それは後続のアクセスに対して有意な速度の増加を提供するキャッシュ、遅いので、おそらくこれはある。)

このDynamicMethodは、最終的トークンのリストを保持DynamicScopeを参照DynamicILGeneratorへの参照を保持しています。これらのトークンの1つは、動的に生成される文字列'System.Dynamic.ExpandoObject' does not contain a definition for 'SomePropName'です。この文字列はメモリ内に存在するため、動的に生成されたコードはRuntimeBinderExceptionに「正しい」メッセージをスロー(およびキャッチ)できます。

全体として、<>p__1フィールドには、約2Kのデータが保持されます(この文字列には172バイトを含みます)。このデータを解放する方法はありません。これは、コンパイラ生成型の静的フィールドが原因です。(もちろん、静的フィールドをnullに設定するためにリフレクションを使用することもできますが、これは現在のコンパイラの実装の詳細に非常に依存し、将来的には破損する可能性が非常に高くなります)。

これまでのところ、dynamicを使用すると、C#コードでプロパティアクセスごとに約2Kのメモリが割り当てられるように見えます。おそらく、これを動的コードを使用する価格と考える必要があります。しかし、(少なくともこの単純な例では)、そのメモリはコードが最初に実行されたときにのみ割り当てられるので、プログラムが実行されるほど長くメモリを使い続けるべきではありません。作業セットを5GBまでプッシュする異なるリークが存在する可能性があります。 (文字列には3つのインスタンスがあるため、foo.SomePropNameを実行する別々のコード行がありますが、foocallを100回呼び出すと3つのインスタンスしか存在しません)。

パフォーマンスを向上させ、 Dictionary<string, string>またはDictionary<string, object>をより単純なキー/値ストアとして使用することを検討してください(コードが書き込まれる方法で可能な場合)。 ExpandoObjectIDictionary<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

だから私は00sか000sこれらの文字列の周りにぶら下がって私が正しく処分されていないいくつかのオブジェクトを持っている必要があります見ていると思いますか?私はダウン・ディープがこれらの行に沿って何か(単純化して言えば)であると考えました:アクセスを試み、例外を捕まえ、対処し、価値を返そうとします。私は 'IDictionary foo = new ExpandoObject();'を実行するときに漏れが報告されていることを(ここでは&他の場所で)見てきましたが、まだ私はそこを掘っていません。私はExpandoObjectのほとんどの用途をDictionaryに変更することに成功しましたが、それはそれほど単純ではない点がいくつかあります。 – AngryPrimate

関連する問題