2013-03-04 4 views
11

Ich habe ein Test, gebe ich zu erwarten, aber das Verhalten des Garbage Collector ist nicht, wie ich vermutet:Garbage Collection sollte Objekt entfernt haben, aber WeakReference.IsAlive noch gibt true zurück

[Test] 
public void WeakReferenceTest2() 
{ 
    var obj = new object(); 
    var wRef = new WeakReference(obj); 

    wRef.IsAlive.Should().BeTrue(); //passes 

    GC.Collect(); 

    wRef.IsAlive.Should().BeTrue(); //passes 

    obj = null; 

    GC.Collect(); 

    wRef.IsAlive.Should().BeFalse(); //fails 
} 

In diesem Beispiel wird die obj Objekt sollte GC'd sein und daher würde ich erwarten, dass die WeakReference.IsAlive Eigenschaft false zurückgibt.

Es scheint, dass, weil die obj Variable im gleichen Umfang wie die GC.Collect deklariert wurde, wird es nicht gesammelt. Wenn ich die Obj-Deklaration und Initialisierung außerhalb der Methode verschiebe, wird der Test bestanden.

Hat jemand eine technische Referenzdokumentation oder Erklärung für dieses Verhalten?

+1

Haben Sie überprüft, wie der IL-Code aussieht? Verhält es sich auch für Release- und Debug-Builds auf die gleiche Weise? –

+3

Meine erste Vermutung ist, dass Compiler/Laufzeit/Prozessor-Optimierungen Sie beißen. Sie stellen fest, dass Sie nie "obj" lesen, so dass es erlaubt ist, die Operationen zwischen den anderen Methodenaufrufen neu zu ordnen. Versuchen Sie etwas wie 'Console.WriteLine (obj == null)' hinzuzufügen, nur um den Compiler daran zu hindern. – Servy

+1

Dieses Beispiel funktioniert auf meinem Computer einwandfrei. Ich benutze 'Console.WriteLine', um den' IsAlive' Parameter zu protokollieren, anstelle von 'Should()' – JaredPar

Antwort

6

Das gleiche Problem wie Sie - mein Test war überall vorbei, außer unter NCrunch (könnte in Ihrem Fall andere Instrumente sein). Hm. Das Debuggen mit SOS ergab zusätzliche Wurzeln, die auf einem Call-Stack einer Testmethode gehalten wurden. Meine Vermutung ist, dass sie ein Ergebnis der Code-Instrumentierung waren, die Compiler-Optimierungen deaktiviert hat, einschließlich solcher, die die Erreichbarkeit von Objekten korrekt berechnen.

Die Heilung hier ist ganz einfach - nicht immer starke Referenzen von einer Methode, die GC und testet für Lebendigkeit. Dies kann leicht mit einer trivialen Hilfsmethode erreicht werden. Die Änderung unten führte dazu, dass Ihr Testfall mit NCrunch bestanden wurde, wo er ursprünglich fehlgeschlagen ist.

[TestMethod] 
public void WeakReferenceTest2() 
{ 
    var wRef2 = CallInItsOwnScope(() => 
    { 
     var obj = new object(); 
     var wRef = new WeakReference(obj); 

     wRef.IsAlive.Should().BeTrue(); //passes 

     GC.Collect(); 

     wRef.IsAlive.Should().BeTrue(); //passes 
     return wRef; 
    }); 

    GC.Collect(); 

    wRef2.IsAlive.Should().BeFalse(); //used to fail, now passes 
} 

private T CallInItsOwnScope<T>(Func<T> getter) 
{ 
    return getter(); 
} 
+0

Danke dafür! Eine sehr elegante Lösung. Ich habe auch NCrunch benutzt. – TechnoTone

+0

Diese Lösung funktionierte für mich, obwohl ich es ein wenig vereinfacht habe. Ich habe die "CallInItsOwnScope" -Operationen in einer separaten Funktion und nicht in einem Lambda platziert, und dies ermöglichte es dem erzwungenen GC, wie erwartet zu arbeiten. – Kent

2

Könnte es sein, dass die Erweiterungsmethode .Should() irgendwie an einer Referenz hängt? Oder vielleicht verursacht ein anderer Aspekt des Test-Frameworks dieses Problem.

(Ich bin dieses Posting als Antwort sonst kann ich nicht einfach nach dem Code!)

ich den folgenden Code versucht haben, und es funktioniert wie erwartet (Visual Studio 2012, .NET 4 zu bauen, Debug und Release, 32 Bit und 64 Bit, läuft auf Windows 7, Quad-Core-Prozessor):

using System; 

namespace Demo 
{ 
    internal class Program 
    { 
     private static void Main(string[] args) 
     { 
      var obj = new object(); 
      var wRef = new WeakReference(obj); 

      GC.Collect(); 
      obj = null; 
      GC.Collect(); 

      Console.WriteLine(wRef.IsAlive); // Prints false. 
      Console.ReadKey(); 
     } 
    } 
} 

Was passiert, wenn Sie diesen Code versuchen?

+2

Wenn 'Should' an einer Referenz festgehalten wird, müsste sie in einer statischen Variablen sein, sonst wäre sie außerhalb des Gültigkeitsbereichs und wäre Müll gesammelt worden. – Servy

+0

Einverstanden, und das scheint unwahrscheinlich. Aber ohne den Quellcode dafür zu sehen, muss ich die Möglichkeit voraussetzen. Obwohl ich denke, dass es wirklich nur ein "undefiniertes Verhalten" in Bezug auf das Timing ist, wann nulled-out-Referenzen tatsächlich zu Garbage Collectable werden, wie Sie vorgeschlagen haben. –

+0

Das "Sollte" stammt aus der FluentAssertions-Bibliothek. Ich bekomme das gleiche Verhalten mit Assert.False statt. – TechnoTone

4

So weit ich weiß, ruft Collect nicht garantiert, dass alle Ressourcen freigegeben werden. Sie machen dem Müllsammler nur einen Vorschlag.

Sie könnten versuchen, es zu zwingen, zu blockieren, bis alle Objekte, indem Sie diese freigegeben werden:

GC.Collect(2, GCCollectionMode.Forced, true); 

Ich gehe davon aus, dass dies nicht unbedingt 100% der Zeit funktionieren könnte. Im Allgemeinen würde ich es vermeiden, irgendeinen Code zu schreiben, der davon abhängt, den Garbage Collector zu beobachten, er ist nicht wirklich dafür ausgelegt, auf diese Weise verwendet zu werden.

9

Es gibt ein paar möglichen Probleme ich sehen kann:

  • ich nicht bewusst bin, irgendetwas in der C# Spezifikation, die erfordert, dass die Lebensdauer von lokalen Variablen begrenzt werden. Ich denke, dass der Compiler die letzte Zuweisung zu obj (null festlegen) freilassen könnte, da kein Codepfad den Wert von obj später nie verwenden würde, aber ich würde das in erwarten Bei einem Nicht-Debug-Build würden die Metadaten anzeigen, dass die Variable nach der Erstellung der schwachen Referenz nie verwendet wird. In einem Debug-Build sollte die Variable im gesamten Funktionsumfang vorhanden sein, aber die obj = null;-Anweisung sollte sie tatsächlich löschen. Nichtsdestotrotz bin ich nicht sicher, dass die C# -Spezifikation verspricht, dass der Compiler die letzte Anweisung nicht auslässt und dennoch die Variable herum hält.

  • Wenn Sie einen gleichzeitigen Garbage Collector verwenden, würde es sein, dass GC.Collect() den sofortigen Start einer Auflistung auslöst, aber dass die Sammlung nicht wirklich abgeschlossen wird, bevor zurückgibt.In diesem Szenario ist es möglicherweise nicht erforderlich, zu warten, bis alle Finalizer ausgeführt werden, und daher ist GC.WaitForPendingFinalizers() möglicherweise übertrieben, aber es würde wahrscheinlich das Problem lösen.

  • Wenn ich den Standard-Garbage Collector verwende, würde ich nicht erwarten, dass die Existenz eines schwachen Verweises auf ein Objekt die Existenz des Objekts in der Art verlängert, wie es ein Finalizer wäre, aber bei Verwendung eines gleichzeitigen Garbage Collectors ist es möglich Die aufgegebenen Objekte, auf die eine schwache Referenz existiert, werden in eine Warteschlange von Objekten mit schwachen Referenzen verschoben, die bereinigt werden müssen, und die Verarbeitung einer solchen Bereinigung findet in einem separaten Thread statt, der gleichzeitig mit allem anderen ausgeführt wird. In diesem Fall wäre ein Aufruf an GC.WaitForPendingFinalizers() erforderlich, um das gewünschte Verhalten zu erreichen.

Hinweis, dass man sollte in der Regel nicht, dass die schwachen Referenzen erwartet, mit einem bestimmten Grad an Aktualität für ungültig erklärt werden, noch sollte man erwarten, dass Target nach IsAlive Berichten wahr Abrufen einer Nicht-Null-Referenz erhalten. Man sollte IsAlive nur in Fällen verwenden, in denen man sich nicht um das Ziel kümmern würde, wenn es noch lebt, sondern daran interessiert zu wissen, dass die Referenz gestorben ist. Wenn zum Beispiel eine Sammlung von WeakReference Objekten vorhanden ist, möchte man möglicherweise periodisch durch die Liste iterieren und WeakReference Objekte entfernen, deren Ziel abgestorben ist. Man sollte auf die Möglichkeit vorbereitet sein, dass WeakReferences länger in der Sammlung bleiben könnte, als es im Idealfall notwendig wäre; Die einzige Konsequenz, wenn sie dies tun, sollte eine leichte Verschwendung von Speicher und CPU-Zeit sein.

0

Ich habe das Gefühl, dass Sie benötigen GC.WaitForPendingFinalizers() zu nennen, wie ich erwarte, dass Woche Referenzen durch den Finalizer-Thread aktualisiert werden.

Ich hatte Probleme mit den vielen Jahren, als ich einen Komponententest schrieb und daran erinnerte, dass WaitForPendingFinalizers() geholfen hat, also machte ich Anrufe zu GC.Collect().

Die Software lief im wirklichen Leben nie durch, aber einen Komponententest zu schreiben, um zu beweisen, dass das Objekt nicht am Leben erhalten wurde, war viel schwieriger, als ich gehofft hatte. (Wir hatten Fehler in der Vergangenheit mit unserem Cache, die ihn am Leben erhalten haben.)