Wird ein Speicherverlust verursacht, wenn ein MemoryStream in .NET nicht geschlossen wird?

111

Ich habe folgenden Code:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

Gibt es eine Chance, dass der von mir zugewiesene MemoryStream später nicht mehr entsorgt wird?

Ich habe eine Peer Review, die darauf besteht, dass ich dies manuell schließe, und ich kann keine Informationen finden, um festzustellen, ob er einen gültigen Punkt hat oder nicht.

Codierer
quelle
41
Fragen Sie Ihren Rezensenten genau, warum er der Meinung ist, dass Sie es schließen sollten. Wenn er über allgemeine bewährte Praktiken spricht, ist er wahrscheinlich schlau. Wenn er davon spricht, früher Speicher freizugeben, liegt er falsch.
Jon Skeet

Antworten:

60

Wenn etwas wegwerfbar ist, sollten Sie es immer entsorgen. Sie sollten eine usingAnweisung in Ihrer bar()Methode verwenden, um sicherzustellen, dass ms2Disposed wird.

Es wird schließlich vom Garbage Collector aufgeräumt, aber es ist immer eine gute Praxis, Dispose aufzurufen. Wenn Sie FxCop für Ihren Code ausführen, wird dies als Warnung gekennzeichnet.

Rob Prouse
quelle
16
Die using-Blockaufrufe verfügen für Sie.
Nick
20
@Grauenwolf: Ihre Behauptung bricht die Kapselung. Als Verbraucher sollte es Ihnen egal sein, ob es sich um ein No-Op handelt: Wenn es IDisposable ist, ist es Ihre Aufgabe, es zu entsorgen ().
Marc Gravell
4
Dies gilt nicht für die StreamWriter-Klasse: Der verbundene Stream wird nur dann entsorgt, wenn Sie den StreamWriter entsorgen. Der Stream wird niemals entsorgt, wenn der Müll gesammelt und der Finalizer aufgerufen wird. Dies ist beabsichtigt.
springy76
4
Ich weiß, dass diese Frage aus dem Jahr 2008 stammt, aber heute haben wir die .NET 4.0-Aufgabenbibliothek. Dispose () ist nicht notwendig in den meisten Fällen , wenn Aufgaben verwenden. Ich würde zwar zustimmen, dass IDisposable bedeuten sollte "Sie sollten dies besser entsorgen, wenn Sie fertig sind", aber das bedeutet das nicht mehr wirklich.
Phil
7
Ein weiteres Beispiel, bei dem Sie IDisposable-Objekte nicht entsorgen sollten, ist HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong. Nur ein weiteres Beispiel von BCL, bei dem IDisposable-Objekte vorhanden sind und Sie nicht entsorgen müssen (oder sollten) es. Dies ist nur zur Erinnerung, dass es normalerweise einige Ausnahmen von der allgemeinen Regel gibt, sogar in BCL;)
Mariusz Pawelski
166

Sie werden nichts verlieren - zumindest in der aktuellen Implementierung.

Durch Aufrufen von Dispose wird der von MemoryStream verwendete Speicher nicht schneller bereinigt. Dadurch wird verhindert, dass Ihr Stream nach dem Aufruf für Lese- / Schreibanrufe geeignet ist, was für Sie möglicherweise nützlich ist oder nicht.

Wenn Sie absolut sicher sind, dass Sie niemals von einem MemoryStream zu einer anderen Art von Stream wechseln möchten, schadet es Ihnen nicht, Dispose nicht aufzurufen. Allerdings ist es im Allgemeinen gute Praxis zum Teil , weil , wenn Sie jemals tun Änderung einen anderen Strom zu verwenden, Sie wollen nicht von einer schwer zu finden Bug gebissen werden , weil Sie den einfachen Weg gewählt haben sehr frühe Phase. (Auf der anderen Seite gibt es das YAGNI-Argument ...)

Der andere Grund dafür ist, dass eine neue Implementierung möglicherweise Ressourcen einführt, die bei Dispose freigegeben würden.

Jon Skeet
quelle
In diesem Fall gibt die Funktion einen MemoryStream zurück, da sie "Daten bereitstellt, die je nach Aufruf von Parametern unterschiedlich interpretiert werden können". Es könnte sich also um ein Byte-Array handeln, war jedoch aus anderen Gründen als MemoryStream einfacher. Es wird also definitiv keine weitere Stream-Klasse sein.
Coderer
In diesem Fall würde ich immer noch versuchen , es nur nach dem allgemeinen Prinzip zu entsorgen - gute Gewohnheiten aufbauen usw. -, aber ich würde mir keine Sorgen machen, wenn es schwierig würde.
Jon Skeet
1
Wenn Sie sich wirklich Sorgen machen, Ressourcen so schnell wie möglich freizugeben, löschen Sie die Referenz sofort nach Ihrem "using" -Block auf Null, damit nicht verwaltete Ressourcen (falls vorhanden) bereinigt werden und das Objekt für die Speicherbereinigung in Frage kommt. Wenn die Methode sofort zurückkehrt, wird dies wahrscheinlich keinen großen Unterschied machen. Wenn Sie jedoch andere Dinge in der Methode ausführen, z. B. mehr Speicher anfordern, kann dies sicherlich einen Unterschied bewirken.
Triynko
@Triynko Nicht wirklich wahr: Siehe: stackoverflow.com/questions/574019/… für Details.
George Stocker
10
Das YAGNI-Argument könnte in beide Richtungen geführt werden - da die Entscheidung, etwas, das implementiert IDisposablewird , nicht zu entsorgen, ein Sonderfall ist, der gegen die üblichen Best Practices verstößt, könnten Sie argumentieren, dass Sie diesen Fall im YAGNI erst dann tun sollten, wenn Sie es wirklich brauchen Prinzip.
Jon Hanna
26

Ja, es gibt ein Leck , abhängig davon, wie Sie LEAK definieren und wie viel SPÄTER Sie meinen ...

Wenn mit Leck gemeint ist, dass "der Speicher zugewiesen bleibt und nicht zur Verwendung verfügbar ist, obwohl Sie ihn nicht mehr verwenden", und mit letzterem meinen Sie jederzeit nach dem Aufruf von dispose, dann kann es ja zu einem Leck kommen, obwohl es nicht dauerhaft ist (dh für die Lebensdauer Ihrer Anwendungslaufzeit).

Um den vom MemoryStream verwendeten verwalteten Speicher freizugeben , müssen Sie die Referenzierung aufheben , indem Sie Ihren Verweis darauf ungültig machen , damit er sofort für die Speicherbereinigung in Frage kommt. Wenn Sie dies nicht tun, erstellen Sie ab dem Zeitpunkt, an dem Sie es verwendet haben, ein temporäres Leck, bis Ihre Referenz den Gültigkeitsbereich verlässt, da der Speicher in der Zwischenzeit nicht für die Zuordnung verfügbar ist.

Der Vorteil der using-Anweisung (gegenüber dem einfachen Aufruf von dispose) besteht darin, dass Sie Ihre Referenz in der using-Anweisung deklarieren können. Wenn die using-Anweisung beendet ist, wird nicht nur dispose aufgerufen, sondern Ihre Referenz verlässt den Gültigkeitsbereich, wodurch die Referenz effektiv ungültig wird und Ihr Objekt sofort für die Speicherbereinigung in Frage kommt, ohne dass Sie daran denken müssen, den Code "reference = null" zu schreiben.

Obwohl es kein klassisches "permanentes" Speicherleck ist, etwas nicht sofort zu entfernen, hat es definitiv den gleichen Effekt. Wenn Sie beispielsweise Ihren Verweis auf den MemoryStream beibehalten (auch nach dem Aufruf von dispose) und etwas weiter unten in Ihrer Methode versuchen, mehr Speicher zuzuweisen, ist der von Ihrem noch referenzierten Speicherstrom verwendete Speicher nicht verfügbar an Sie, bis Sie die Referenz ungültig machen oder sie außerhalb des Gültigkeitsbereichs liegt, obwohl Sie dispose aufgerufen haben und damit fertig sind.

Triynko
quelle
6
Ich liebe diese Antwort. Manchmal vergessen die Menschen die doppelte Pflicht der Nutzung: eifrige Rückgewinnung von Ressourcen und eifrige Dereferenzierung.
Kit
1
Obwohl ich gehört habe, dass der C # -Compiler im Gegensatz zu Java "letzte mögliche Verwendung" erkennt, kann die Variable nach ihrer letzten Referenz möglicherweise für die Speicherbereinigung in Frage kommen, wenn sie nach ihrer letzten Referenz den Gültigkeitsbereich verlässt. . bevor es tatsächlich aus dem Rahmen geht. Siehe stackoverflow.com/questions/680550/explicit-nulling
Triynko
2
Der Müllsammler und der Jitter funktionieren nicht so. Scope ist eine Sprachkonstruktion und nicht etwas, dem die Laufzeit gehorcht. Wahrscheinlich verlängern Sie wahrscheinlich die Zeit, in der sich die Referenz im Speicher befindet, indem Sie .Dispose () aufrufen, wenn der Block endet. Siehe ericlippert.com/2015/05/18/…
Pablo Montilla
8

Das Aufrufen .Dispose()(oder Umschließen mit Using) ist nicht erforderlich.

Der Grund, den Sie aufrufen, .Dispose()besteht darin , die Ressource so schnell wie möglich freizugeben .

Denken Sie beispielsweise an den Stack Overflow-Server, auf dem nur ein begrenzter Arbeitsspeicher und Tausende von Anfragen eingehen. Wir möchten nicht auf die geplante Speicherbereinigung warten, sondern diesen Speicher so schnell wie möglich freigeben, damit er verfügbar ist für neue eingehende Anfragen.

Jeff Atwood
quelle
24
Wenn Sie Dispose in einem MemoryStream aufrufen, wird jedoch kein Speicher freigegeben. Tatsächlich können Sie immer noch auf die Daten in einem MemoryStream zugreifen, nachdem Sie Dispose aufgerufen haben - probieren Sie es aus :)
Jon Skeet
12
-1 Während dies für einen MemoryStream zutrifft, ist dies als allgemeiner Rat einfach falsch. Entsorgen besteht darin, nicht verwaltete Ressourcen wie Dateihandles oder Datenbankverbindungen freizugeben . Der Speicher fällt nicht in diese Kategorie. Sie sollten fast immer auf die geplante Speicherbereinigung warten, um Speicherplatz freizugeben.
Joe
1
Was bringt es, einen Codierungsstil für das Zuweisen und Entsorgen von FileStreamObjekten und einen anderen für MemoryStreamObjekte zu verwenden?
Robert Rossney
3
Ein FileStream umfasst nicht verwaltete Ressourcen, die beim Aufruf von Dispose sofort freigegeben werden können. Ein MemoryStream hingegen speichert ein verwaltetes Byte-Array in seiner _buffer-Variablen, die zum Zeitpunkt der Entsorgung nicht freigegeben wird. Tatsächlich wird der _buffer in der Dispose-Methode von MemoryStream, die eine SHAMEFUL BUG IMO ist, nicht einmal auf Null gesetzt, da durch das Aufheben der Referenz der Speicher zum Zeitpunkt der Entsorgung für GC in Frage kommen könnte. Stattdessen bleibt eine verweilende (aber entsorgte) MemoryStream-Referenz im Speicher. Sobald Sie es entsorgen, sollten Sie es daher auch auf Null setzen, wenn es noch im Geltungsbereich ist.
Triynko
@Triynko - "Sobald Sie es entsorgen, sollten Sie es auch auf Null setzen, wenn es noch im Geltungsbereich ist" - Ich bin anderer Meinung. Wenn es nach dem Aufruf von Dispose erneut verwendet wird, führt dies zu einer NullReferenceException. Wenn es nach Dispose nicht mehr verwendet wird, muss es nicht auf Null gesetzt werden. Der GC ist klug genug.
Joe
8

Dies ist bereits beantwortet, aber ich möchte nur hinzufügen, dass das gute altmodische Prinzip des Versteckens von Informationen bedeutet, dass Sie möglicherweise zu einem späteren Zeitpunkt eine Umgestaltung wünschen:

MemoryStream foo()
{    
    MemoryStream ms = new MemoryStream();    
    // write stuff to ms    
    return ms;
}

zu:

Stream foo()
{    
   ...
}

Dies unterstreicht, dass es Anrufern egal sein sollte, welche Art von Stream zurückgegeben wird, und ermöglicht es, die interne Implementierung zu ändern (z. B. beim Verspotten für Unit-Tests).

Sie müssen dann in Schwierigkeiten geraten, wenn Sie Dispose in Ihrer Balkenimplementierung nicht verwendet haben:

void bar()
{    
    using (Stream s = foo())
    {
        // do stuff with s
        return;
    }
}
Joe
quelle
5

Alle Streams implementieren IDisposable. Wickeln Sie Ihren Speicher-Stream in eine using-Anweisung ein, und es wird Ihnen gut gehen. Der using-Block stellt sicher, dass Ihr Stream geschlossen und entsorgt wird, egal was passiert.

Wo immer du Foo aufrufst, kannst du es mit (MemoryStream ms = foo ()) machen und ich denke, du solltest immer noch in Ordnung sein.

Nick
quelle
1
Ein Problem, auf das ich bei dieser Angewohnheit gestoßen bin, ist, dass Sie sicher sein müssen, dass der Stream nirgendwo anders verwendet wird. Zum Beispiel habe ich einen JpegBitmapDecoder erstellt, der auf einen MemoryStream verweist, und Frames [0] zurückgegeben (ich dachte, er würde die Daten in seinen eigenen internen Speicher kopieren), aber festgestellt, dass die Bitmap nur 20% der Zeit angezeigt wird - es stellte sich heraus, dass dies daran lag Ich habe den Speicherstrom entsorgt.
Devios1
Wenn Ihr Speicherstrom bestehen bleiben muss (dh ein using-Block macht keinen Sinn), sollten Sie Dispose aufrufen und die Variable sofort auf null setzen. Wenn Sie es entsorgen, ist es nicht mehr für die Verwendung vorgesehen. Sie sollten es daher auch sofort auf null setzen. Was Chaiguy beschreibt, klingt nach einem Problem der Ressourcenverwaltung, da Sie einen Verweis auf etwas nur dann verteilen sollten, wenn das Objekt, an das Sie es übergeben, die Verantwortung für die Entsorgung übernimmt und das Objekt, das den Verweis verteilt, weiß, dass es nicht mehr dafür verantwortlich ist dabei.
Triynko
2

Sie werden keinen Speicher verlieren, aber Ihr Code-Prüfer zeigt korrekt an, dass Sie Ihren Stream schließen sollten. Es ist höflich, dies zu tun.

Die einzige Situation, in der Sie möglicherweise Speicher verlieren, besteht darin, dass Sie versehentlich einen Verweis auf den Stream hinterlassen und ihn niemals schließen. Sie sind undicht noch nicht wirklich Speicher, aber Sie sind Verlängerung unnötig die Menge an Zeit , die Sie verwenden es verlangen.

OwenP
quelle
1
> Sie verlieren immer noch nicht wirklich Speicher, aber Sie verlängern unnötig die Zeit, die Sie angeblich verwenden. Bist du sicher? Dispose gibt keinen Speicher frei und ein später Aufruf der Funktion kann die Zeit verlängern, in der er nicht erfasst werden kann.
Jonathan Allen
2
Ja, Jonathan hat Recht. Wenn Sie einen Aufruf von Dispose spät in der Funktion platzieren, könnte der Compiler tatsächlich denken, dass Sie sehr spät in der Funktion auf die Stream-Instanz zugreifen müssen (um sie zu schließen). Dies könnte schlimmer sein, als dispose überhaupt nicht aufzurufen (wodurch ein später Verweis auf die Stream-Variable vermieden wird), da ein Compiler andernfalls früher in der Funktion einen optimalen Release-Punkt (auch als "Punkt der letztmöglichen Verwendung" bezeichnet) berechnen könnte .
Triynko
2

Ich würde empfehlen, den MemoryStream hauptsächlich aus Gründen der Konsistenz bar()in eine usingAnweisung einzuschließen :

  • Derzeit gibt MemoryStream keinen freien Speicherplatz frei .Dispose(), aber es ist möglich, dass dies irgendwann in der Zukunft der Fall ist oder dass Sie (oder eine andere Person in Ihrem Unternehmen) ihn durch Ihren eigenen benutzerdefinierten MemoryStream usw. ersetzen.
  • Es ist hilfreich, ein Muster in Ihrem Projekt zu erstellen, um sicherzustellen, dass alle Streams entsorgt werden. Die Grenze wird fester gezogen, indem gesagt wird, dass alle Streams entsorgt werden müssen, anstatt dass einige Streams entsorgt werden müssen, bestimmte jedoch nicht. ...
  • Wenn Sie den Code jemals ändern, um andere Arten von Streams zurückzugeben, müssen Sie ihn ändern, um ihn trotzdem zu entsorgen.

Eine andere Sache, die ich normalerweise in Fällen wie foo()dem Erstellen und Zurückgeben eines IDisposable mache, ist sicherzustellen, dass ein Fehler zwischen dem Erstellen des Objekts und dem returndurch eine Ausnahme abgefangen wird, das Objekt entsorgt und die Ausnahme erneut auslöst:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}
Chris R. Donnelly
quelle
1

Wenn ein Objekt IDisposable implementiert, müssen Sie die .Dispose-Methode aufrufen, wenn Sie fertig sind.

In einigen Objekten bedeutet Entsorgen dasselbe wie Schließen und umgekehrt. In diesem Fall ist beides gut.

Nun, für Ihre spezielle Frage, nein, Sie werden keinen Speicher verlieren.

Lasse V. Karlsen
quelle
3
"Muss" ist ein sehr starkes Wort. Wann immer es Regeln gibt, lohnt es sich, die Konsequenzen eines Verstoßes zu kennen. Für MemoryStream gibt es nur sehr wenige Konsequenzen.
Jon Skeet
-1

Ich bin kein .net-Experte, aber vielleicht liegt das Problem hier in den Ressourcen, nämlich dem Dateihandle und nicht im Speicher. Ich denke, der Garbage Collector wird den Stream irgendwann freigeben und das Handle schließen, aber ich denke, es wäre immer die beste Vorgehensweise, ihn explizit zu schließen, um sicherzustellen, dass Sie den Inhalt auf die Festplatte spülen.

Steve
quelle
Ein MemoryStream befindet sich ausschließlich im Arbeitsspeicher - hier gibt es kein Dateihandle.
Jon Skeet
-2

Die Entsorgung nicht verwalteter Ressourcen ist in Müllsammelsprachen nicht deterministisch. Selbst wenn Sie Dispose explizit aufrufen, haben Sie absolut keine Kontrolle darüber, wann der Sicherungsspeicher tatsächlich freigegeben wird. Dispose wird implizit aufgerufen, wenn ein Objekt den Gültigkeitsbereich verlässt, sei es durch Beenden einer using-Anweisung oder durch Aufrufen des Callstacks von einer untergeordneten Methode. Dies alles gesagt, manchmal kann das Objekt tatsächlich ein Wrapper für eine verwaltete Ressource (z. B. Datei) sein. Aus diesem Grund empfiehlt es sich, in finally-Anweisungen explizit zu schließen oder die using-Anweisung zu verwenden. Prost


quelle
1
Nicht genau richtig. Dispose wird beim Beenden einer using-Anweisung aufgerufen. Dispose wird nicht aufgerufen, wenn ein Objekt gerade den Gültigkeitsbereich verlässt.
Alexander Abramov
-3

MemorySteram ist nichts anderes als ein Array von Bytes, bei denen es sich um verwaltete Objekte handelt. Vergessen Sie zu entsorgen oder zu schließen, dies hat keine anderen Nebenwirkungen als über den Kopf der Finalisierung.
Überprüfen Sie einfach die Konstruktor- oder Spülmethode von MemoryStream im Reflektor, und es wird klar, warum Sie sich keine Gedanken über das Schließen oder Entsorgen machen müssen, außer nur, um bewährte Verfahren zu befolgen.

user1957438
quelle
6
-1: Wenn Sie eine 4+ Jahre alte Frage mit einer akzeptierten Antwort posten möchten, versuchen Sie bitte, sie nützlich zu machen.
Tieson T.