2017-02-28 17 views
4

Windows 서비스로 실행되는 상속 된 .NET 4.0 응용 프로그램이 있습니다. 어떤 스트레칭으로 .NET 전문가가 아니지만 30 년 이상 코드를 작성한 후에는 내 길을 찾을 수있는 방법을 알고 있습니다..NET4 ExpandoObject 사용 메모리 누수

서비스가 처음 시작될 때 약 70MB 개인 작업 세트에서 시계가 시작됩니다. 서비스가 길어질수록 더 많은 메모리가 사용됩니다. 이 증가는 앉아서보고있는 동안 눈치 채지 못했지만 응용 프로그램을 오랜 시간 (100 일 이상) 실행 한 후에는 최대 여러 GB (현재 레코드가 5GB) 인 경우를 보았습니다. ANTS 메모리 프로파일 러를 실행중인 인스턴스에 연결하여 ExpandoObject의 사용이 GC로 정리되지 않은 여러 메가 바이트의 문자열을 차지하는 것으로 나타났습니다. 다른 누출 가능성이 있지만, 가장 눈에 띄기 때문에 먼저 공격을당했습니다.

나는 ExpandoObject의 "일반적인"사용이 동적으로 할당 된 속성을 읽을 때 (그러나 쓰지는 않음) 내부 RuntimeBinderException을 생성한다는 것을 다른 SO 게시물에서 알게되었습니다.

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

"버그"는 보는 사람의 눈에, 나는 가정 (내 눈이 버그를 말한다). 내가 놓친 게 있니? 그룹의 .NET 마스터가 정상적이고 기대합니까? 물건을 치우라고 말할 수있는 방법이 있습니까? 동적/ExpandoObject를 처음부터 사용하지 않는 가장 좋은 방법은 무엇입니까?

+0

: ExpandoObjectIDictionary<string, object> 그래서 다음과 같은 작은 재 작성이 동일한 출력을 생성하지만, 동적 코드의 오버 헤드를 방지 구현합니다? –

+0

예,하지만 내가 얼마나 많은 스레드를 끄고 관계없이 예외 문자열의 3 인스턴스를 보여줍니다. – AngryPrimate

답변

4

이것은 동적 속성 액세스를 위해 컴파일러에서 생성 된 코드에 의해 수행 된 캐싱 때문인 것으로 보입니다. VS2015 및 .NET 4.6의 출력으로 수행되는 분석은 다른 출력을 생성 할 수 있습니다.

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에 대한 참조를 보유하고있다. 이 토큰 중 하나는 동적으로 생성 된 문자열 'System.Dynamic.ExpandoObject' does not contain a definition for 'SomePropName'입니다. 이 문자열은 동적으로 생성 된 코드가 RuntimeBinderException에 "올바른"메시지를 포함하여 전달 (catch) 할 수 있도록 메모리에 있습니다.

전체적으로 <>p__1 필드는 약 2KB의 데이터를 보존합니다 (이 문자열의 경우 172 바이트 포함). 이 데이터는 컴파일러에서 생성 된 유형의 정적 필드를 기반으로하기 때문에이 데이터를 자유롭게 사용할 수있는 방법은 없습니다.(물론 정적 필드를 null으로 설정하기 위해 리플렉션을 사용할 수는 있지만 현재 컴파일러의 구현 세부 사항에 매우 의존적이어서 향후에는 중단 될 가능성이 높습니다.)

내가 본 것부터 지금까지는 dynamic을 사용하면 C# 코드에서 속성 액세스 당 약 2K 개의 메모리가 할당됩니다. 동적 코드를 사용할 때의 가격을 고려해야 할 것입니다. 그러나 (적어도이 단순화 된 예제에서) 메모리는 코드가 처음 실행될 때만 할당되므로 프로그램 실행 시간이 길어질수록 더 많은 메모리를 사용하지 않아야합니다. 작업 세트를 5GB까지 밀어 넣는 다른 누수가있을 수 있습니다. (을 실행하는 세 개의 별도 코드 줄이 있기 때문에 문자열의 인스턴스가 세 개이지만 foocall을 100 번 호출해도 여전히 세 개의 인스턴스가 있습니다.)

성능을 향상시키고 메모리 사용을 줄이려면 Dictionary<string, string> 또는 Dictionary<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 도움이 되었다면 upvote 또는 대답을 수락하십시오! –

+0

그래서 나는 00s 또는 000s이 문자열 주위에 매달려 내가 제대로 처분지고 있지 않은 일부 개체가 있어야 볼 수있을 것 같아요? 나는 아래쪽이 그 선들을 따라 무언가 (단순하게 말하면)라고 생각했다 : 접근을 시도하고, 예외를 찾아 내고, 가치를 반환하고, 가치를 되돌려 보아라. 나는 IDictionary foo = new ExpandoObject();'를 수행 할 때 누수가보고된다는 게시물 (여기 & 다른 곳)을 보았습니다. 그러나 아직 파헤 치지 않았습니다. 나는 ExpandoObject의 대부분의 사용을 사전으로 바꾸는데 성공했지만, 그다지 간단하지 않은 몇 가지 점이있다. – AngryPrimate