2012-10-20 3 views
6

Ich habe ein Problem mit finalisierbaren Objekten festgestellt, das nicht von GC erfasst wird, wenn Dispose() nicht explizit aufgerufen wurde. Ich weiß, dass ich Dispose() explizit aufrufen sollte, wenn ein Objekt IDisposable implementiert, aber ich dachte immer, dass es sicher ist, sich auf Framework zu verlassen, und wenn ein Objekt referenziert wird, kann es gesammelt werden.Warum wird Objekt mit Finalizer nicht erfasst, auch wenn es nicht gerootet wird?

Aber nach einigen Versuchen mit windbg/sos/sosex Ich habe festgestellt, dass, wenn GC.SuppressFinalize() nicht für finalizable Objekt aufgerufen wurde es nicht gesammelt hat, auch wenn es unbewurzelten wird. Wenn Sie also endliche Objekte (DbConnection, FileStream, etc.) ausgiebig verwenden und diese nicht explizit freigeben, können Sie auf einen zu hohen Speicherverbrauch oder sogar auf OutOfMemoryException stoßen.

Hier ist eine Beispielanwendung:

public class MemoryTest 
{ 
    private HundredMegabyte hundred; 

    public void Run() 
    { 
     Console.WriteLine("ready to attach"); 
     for (var i = 0; i < 100; i++) 
     { 
      Console.WriteLine("iteration #{0}", i + 1); 
      hundred = new HundredMegabyte(); 
      Console.WriteLine("{0} object was initialized", hundred); 
      Console.ReadKey(); 
      //hundred.Dispose(); 
      hundred = null; 
     } 
    } 

    static void Main() 
    { 
     var test = new MemoryTest(); 
     test.Run(); 
    } 
} 

public class HundredMegabyte : IDisposable 
{ 
    private readonly Megabyte[] megabytes = new Megabyte[100]; 

    public HundredMegabyte() 
    { 
     for (var i = 0; i < megabytes.Length; i++) 
     { 
      megabytes[i] = new Megabyte(); 
     } 
    } 

    public void Dispose() 
    { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 

    ~HundredMegabyte() 
    { 
     Dispose(false); 
    } 

    private void Dispose(bool disposing) 
    { 
    } 

    public override string ToString() 
    { 
     return String.Format("{0}MB", megabytes.Length); 
    } 
} 

public class Megabyte 
{ 
    private readonly Kilobyte[] kilobytes = new Kilobyte[1024]; 

    public Megabyte() 
    { 
     for (var i = 0; i < kilobytes.Length; i++) 
     { 
      kilobytes[i] = new Kilobyte(); 
     } 
    } 
} 

public class Kilobyte 
{ 
    private byte[] bytes = new byte[1024]; 
} 

Auch nach 10 Iterationen Sie, dass der Speicherverbrauch zu hoch ist (von 700 MB bis 1 GB) und bekommt sogar noch höher mit mehr Iterationen finden. Nach dem Anhängen an den Prozess mit WinDBG können Sie feststellen, dass alle großen Objekte nicht verwaltet, aber nicht gesammelt werden.

Situation ändert sich, wenn Sie SuppressFinalize() explizit aufrufen: Speicherverbrauch ist stabil um 300-400MB auch unter Hochdruck und WinDBG zeigt, dass es keine unrooted Objekte gibt, Speicher ist frei.

Die Frage ist also: Ist es ein Fehler im Framework? Gibt es eine logische Erklärung?

Weitere Einzelheiten:

nach jeder Iteration, windbg zeigt, dass:

  • Finalisierungsschlange leer
  • freachable Warteschlange leer ist
  • Generation 2-Objekte enthält (Hundert) aus früheren Iterationen
  • Objekte aus früheren Iterationen sind nicht entfernt
+0

Ich habe versucht, es zu laufen, und ich habe es bis zu 700mb, an dem Punkt es auf 100mb oder so von selbst abgeschnitten. (Ich habe einfach weiter gedrückt und beobachtete die Speicherauslastung im Task-Manager). Running Windows 8. – keyboardP

+0

Sehr interessant ... Ich habe es auf win7 x64 getestet. Die ersten 10 Iterationen mit einer Sekunde Pause, danach einfach einen beliebigen Schlüssel halten ... outofmemory – 6opuc

+0

Noch einmal versucht, aber zu 100 Iterationen ohne Ausnahme. Auch 64-Bit, aber ziemlich interessant. Vielleicht kann jemand anderes auch Ergebnisse testen und veröffentlichen. – keyboardP

Antwort

7

Ein Objekt mit einem Finalizer verhält sich nicht wie ein Objekt, dem ein Objekt fehlt.

Wenn ein GC auftritt und SuppressFinalize nicht aufgerufen wurde, kann der GC die Instanz nicht erfassen, da der Finalizer ausgeführt werden muss. Daher wird der Finalizer ausgeführt, und die Objektinstanz wird auf Generation 1 hochgestuft (Objekt, das einen ersten GC überlebte), selbst wenn es bereits keine lebende Referenz mehr gibt.

Objekte der ersten Generation (und Gen2) gelten als langlebig und werden nur dann für die Speicherbereinigung berücksichtigt, wenn ein Gen1-GC nicht ausreicht, um genügend Speicher freizugeben. Ich denke, während Ihres Tests ist Gen1 GC immer ausreichend.

Dieses Verhalten hat Auswirkungen auf die GC-Leistung, da es die Optimierung durch mehrere Generationen negiert (Sie haben Objekte mit kurzer Dauer in der gen1).

Im Wesentlichen, einen Finalizer zu haben und zu verhindern, dass der GC sie aufruft, wird immer schon Tote Objekte auf den langlebigen Heap befördern, was keine gute Sache ist.

Sie daher richtig Ihre IDisposable Objekte entsorgen sollten und vermeiden Sie Finalizer, wenn nicht notwendig

Edit (und ggf. implementieren IDisposable und GC.SuppressFinalize nennen.): Ich habe nicht gelesen das Codebeispiel gut Genug: Ihre Daten sehen so aus, als ob sie im Large Object Heap (LOH) liegen sollen, aber tatsächlich nicht: Sie haben viele kleine Arrays von Referenzen, die am Ende des Baumes kleine Arrays von Bytes enthalten.

Das Einfügen von Objekten mit kurzer Dauer in die LOH ist noch schlimmer, da sie nicht komprimiert werden ... Und daher könnte OutOfMemory mit viel freiem Speicher ausgeführt werden, wenn die CLR kein leeres Speichersegment finden kann lang genug, um einen großen Datenblock zu enthalten.

+0

+1, da ich jeder Aussage zustimme, aber ich möchte warten, wenn jemand auf die gleichen Probleme gestoßen ist. Diese Beispielanwendung ist nur eine vereinfachte Version unserer Produktionsumgebung, in der einige der verwendeten Frameworks (EF, spring.net, log4net) nicht Dispose() für Streams und sqlcommands aufrufen ... – 6opuc

+0

Ich habe Ihren Vorschlag überprüft etwa 2. Generation und festgestellt, dass selbst wenn Finalizer auskommentiert ist, die meisten Objekte in der 2. Generation zugewiesen (oder verschoben) werden. Die 2. Generation ist also definitiv nicht die Ursache für dieses Problem. – 6opuc

+0

Sind Sie sicher, dass Sie einen GC ausgelöst haben? Die CLR löst nur dann einen aus, wenn der Speicher nicht ausreicht. Versuchen Sie, GC.Collect() aufzurufen, um sicherzustellen, dass es ausgeführt wird. – Eilistraee

1

Ich denke, die Idee dahinter ist, wenn Sie IDisposable implementieren, liegt daran, dass Sie nicht verwaltete Ressourcen verwalten und Ihre Ressource manuell entsorgen müssen.

Wenn der GC Dispose aufrufen oder versuchen würde, ihn loszuwerden, würde er das nicht verwaltete Material ebenfalls löschen, was sehr wohl woanders verwendet werden kann, der GC kann das nicht wissen.

Wenn der GC das nicht gerootete Objekt entfernen sollte, würden Sie den Hinweis auf nicht verwaltete Ressourcen verlieren, was zu Speicherlecks führen würde.

Also ... Sie sind verwaltet, oder Sie nicht. Es gibt einfach keinen guten Weg für den GC, nicht verarbeitete IDisposables zu handhaben.

Verwandte Themen