Laden...

MemoryLeak "behebt" sich selbst durch Memory-Snapshot

Erstellt von Palladin007 vor einem Jahr Letzter Beitrag vor einem Jahr 741 Views
Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr
MemoryLeak "behebt" sich selbst durch Memory-Snapshot

Guten Mittag,

es geht um eine WPF-Anwendung.
Ich suche gerade ein MemoryLeak, der scheinbar nur auftritt, wenn es um sehr viele Daten geht, also auch die Auswirkungen sehr groß sind.

Dabei ist mir aufgefallen, dass ich mit Hilfe der Diagnostic Tools von Visual Studio den Leak problemlos reproduzieren kann (mehrere Instanzen einer Klasse, wo nur eine sein sollte), doch sobald ich mit dem .NET Memory Profiler einen Snapshot erstelle, sind die überflüssigen Instanzen weg und der Speicher freigegeben.
Eventuell hängt das mit großen zirkulären Referenzen zusammen? Heißt: Ein "Haupt"-Objekt referenziert sehr viele "Detail"-Objekte, die auf das "Haupt"-Objekt verweisen.

Ich kann nicht ganz sagen, ob es wirklich keine Referenzen mehr gibt, daher die Frage:
Kann es sein, dass in einem Fall mit sehr vielen Objekte im "Referenz-Kreis" der GC nicht hinterher kommt und einfach zu lange braucht?
Und wenn ja, wie gehe ich damit am besten um, kann ich den Fall prüfen und ggf. ausschließen?

Und ist es möglich, dem GC die Objekte, die ich definitiv nicht mehr brauche, mitzugeben und sie so "löschen" zu lassen?

4.928 Beiträge seit 2008
vor einem Jahr

Kannst du nicht bei den "Detail"-Objekten die Referenz für das Hauptobjekt auf null setzen, sobald du das Hauptobjekt nicht mehr benötigst?

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

Da bin ich gerade dran, ist aber nicht "mal eben so" gemacht, weil der Kreis etwas größer ist.

Ich hatte gehofft, man kann dem GC einen ganzen Objekt-Baum mitgeben, zumindest erinnere ich mich, mal etwas davon gelesen zu haben.
Oder das war ein Issue, wo jemand nach genau so einem Feature gefragt hat.
Oder ich werfe irgendetwas anderes ganz durcheinander 😁

Ich teste das Mal und eine andere Theorie, dann melde ich mich wieder.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

Kannst du nicht bei den "Detail"-Objekten die Referenz für das Hauptobjekt auf null setzen, sobald du das Hauptobjekt nicht mehr benötigst?

Ergebnis: Es liegt sehr viel weniger im RAM, weil ich jetzt alles auf null gesetzt habe, was ich nur finden konnte, durch alle Klassen hindurch.
Die Instanzen, die nicht da sein sollten, gibt's allerdings immer noch und ein Snapshot vom .NET Memory Profiler "beseitigt" die Instanzen immer noch.

Meine zweite Theorie war scheinbar erfolgreicher, aber auch nur manchmal und ein Snapshot vom .NET Memory Profiler "behebt" das Problem immer noch.
Aber gut, das heißt wohl, dass das Problem doch irgendwo in meinem Code liegt 😁

4.928 Beiträge seit 2008
vor einem Jahr

Wenn durch den .NET Memory Profiler Speicher freigegeben wird, dann teste mal, ob auch ein GC.Collect (z.B. durch einen expliziten Button-Click) diesen freigibt.
Es wäre eigenartig, wenn es nicht so wäre.
Du darfst aber auch nicht erwarten, daß beim automatischen GC-Durchlauf jeder mögliche Speicher freigegeben wird, erst wenn der Speicher an die Grenzen stößt, wird mehr freigegeben (d.h. mehr Prozessorzeit dafür aufgebracht).

6.910 Beiträge seit 2009
vor einem Jahr

Hallo Palladin007,

.NET Memory Profiler einen Snapshot erstelle, sind die überflüssigen Instanzen weg und der Speicher freigegeben.

Die meisten dieser Profiler führen eine kompletten GC durch (also inkl. Gen-2 und LOH) und haken sich in den Informationsstrom vom GC rein um so an die Ojekte zu kommen.
Genau dieses Verhalten hast du beobachtet.

Versuch dich einmal an PerfView, das bietet i.d.R. mehr Infos / Möglichkeiten. Aber die UX ist sehr bescheiden...

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

Die meisten dieser Profiler führen eine kompletten GC durch (also inkl. Gen-2 und LOH) und haken sich in den Informationsstrom vom GC rein um so an die Ojekte zu kommen.
Genau dieses Verhalten hast du beobachtet.

Versuch dich einmal an PerfView, das bietet i.d.R. mehr Infos / Möglichkeiten. Aber die UX ist sehr bescheiden...

Das erklärt's ein bisschen, ich hab testweise nur ein einfaches GC.Collect gemacht, das geht vermutlich nicht weit genug?

Und danke für den Tipp - schau ich mir morgen an.

Wenn durch den .NET Memory Profiler Speicher freigegeben wird, dann teste mal, ob auch ein GC.Collect (z.B. durch einen expliziten Button-Click) diesen freigibt.

Das hatte ich mir auch schon gedacht und GC.Collect eingefügt - allerdings nicht durch einen Button-Klick, sondern direkt nachdem alles weg geworfen wurde.
Gebracht hat es nichts, aber ich teste Morgen mal mit einem eigenen Button, vielleicht macht das einen Unterschied.

erst wenn der Speicher an die Grenzen stößt, wird mehr freigegeben

Ich hätte nicht gedacht, dass er (in meinem Fall) knapp 3GB einfach so liegen lässt.
Aber gut, ich hab auch (noch) keine Ahnung, wie der genau arbeitet - das Buch liegt hier, aber die Zeit fehlt noch ^^

4.928 Beiträge seit 2008
vor einem Jahr

Bei GC.Collect(Int32) kannst du die Generation einstellen, probiere mal GC.Collect(GC.MaxGeneration) (schreib auch mal, welchen Wert MaxGeneration bei dir hat).

PS: Überprüfe auch mal den Wert von GCSettings.LatencyMode (der sollte bei einem normalen Arbeitsplatzrechner auf Interactive stehen - und nicht auf einer der LowLatency-Werte).

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

GC.MaxGeneration = 2
GCSettings.LatencyMode = Interactive

GC.Collect(GC.MaxGeneration) hat auch nichts gebracht.
Manchmal gibt er es auch von alleine frei, ich weiß allerdings nicht, wann und ob das die Daten vom aktuellen Dialog sind, den ich gerade verlasse, oder die von irgendwann vorher.
Wenn ich einen Button verwendet, um GC.Collect(GC.MaxGeneration) aufzurufen, gibt es meistens ungefähr die Menge frei, die ich erwarten würde.

Aber warum?

Ich hab jetzt folgendes eingebaut:


_ = Task.Delay(1000, default)
    .ContinueWith(_ => GC.Collect(GC.MaxGeneration), default(CancellationToken));

Das funktioniert so halb, es gibt ab dem zweiten Dialog-Verlassen wieder frei.

Ich schau mir jetzt PerfView an, vielleicht kann mir das ja etwas mehr verraten, vermutlich habe ich doch irgendwo ein gemeines Problem und ich jage hier unnötig dem GC hinterher.

6.910 Beiträge seit 2009
vor einem Jahr

Hallo Palladin007,


GC.Collect();
GC.WaitForPendingFinalzers();
GC.Collect();

unter Angabe der max. Geneartion räumt den Speicher ordentlich auf.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

GC.Collect(GC.MaxGeneration);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration);

Warum GC.MaxGeneration? Einfach weil viel hilft viel 😁
Ich will erst einmal sehen, ob es überhaupt etwas bringt, danach schaue ich mir an, ob GC.MaxGeneration zu viel ist oder nicht.

Aber tatsächlich hilft es, aber anders als erwartet.
Ich beobachte jetzt zwei Dinge:* Die RAM-Auslastung geht ca. eine Sekunde (ich hab's mit dem Delay eingebaut) nach dem Verlassen des Dialogs wieder runter

  • Die Kurve ist nach den ersten ein/zwei Mal Dialog anzeigen und verlassen sehr viel gleichmäßiger, wie als würden Objekte gecacht werden, was ich aber nicht mache

Gerader der zweite Punkt verwundert mich - hast Du dazu eine Erklärung?
Das GC gibt bei jedem Mal Verlassen des Dialogs sehr viel weniger frei, als ich erwarten würde, aber beim erneuten anzeigen, wird auch sehr viel weniger benötigt.
Könnte das mit SQLite zusammenhängen, dass der - ähnlich dem SQLServer - anfängt, die Daten im RAM vorzuhalten, um den Zugriff zu optimieren?

Aber so wie das aussieht, übernehme ich das für alle Dialoge, dass nach jedem Dialog verlassen erst einmal aufgeräumt wird.
Ich teste nur noch, ob auch ohne GC.MaxGeneration ausreicht und das Delay kann vermutlich auch raus.
Ob das eine gute Lösung ist, weiß ich nicht, aber sie hilft und da ich etwas unter Druck stehe, ist es zumindest eine vorübergehend geeignete Lösung - denke ich.

PS:
Mit PerfView hab ich ein paar Dumbs erstellt, nur werde ich daraus nicht schlau.
Bisher kannte ich das Tool nicht und es scheint auch einiges an Einarbeitung zu benötigen, doch die Zeit habe ich leider nicht.
Ich würde daher eine schnelle Lösung bevorzugen, auch wenn es nur ein Flickenteppich ist, aber dann kann ich es später nochmal angehen.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

private static async void TriggerDelayedFullGC()
{
    await Task.Delay(100, default).ConfigureAwait(false);

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

Das tut's auch.
So baue ich es erst einmal ein und schau später nochmal.

6.910 Beiträge seit 2009
vor einem Jahr

Hallo Palladin007,

GC.Collect bitte nur als Notlösung betrachten, da dies die interne Arbeit und Tuning vom GC durcheinander bringt.

PerfView ist mächtig, aber nicht trivial und intuitiv.
Wenn du willst und es gestattet ist, so kann ich mir die Dumps einmal anschauen und dann eine grobe Art Anleitung dazu erstellen.
Aber ich muss bei PerfView auch immer wieder "reinkommen", da ich es recht schnell wieder vergesse 😉

mfG Gü

PS: aber erst am Montag, übers WE bin ich nicht da.

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr
GC.Collect  

bitte nur als Notlösung betrachten, da dies die interne Arbeit und Tuning vom GC durcheinander bringt.

Leider sehe ich aktuell keine andere Option (außer das manuell alles auf null setzen), der Zeitplan ist schon lange überschritten und der Kunde hat auch nur noch diesen Monat 😠
Da sind mir Notlösungen lieber, als 2,5 GB im RAM versauern zu lassen und es soll im Anschluss sowieso weiter gehen, dann aber mit ruhigerem Zeitplan.

PerfView ist mächtig, aber nicht trivial und intuitiv.
Wenn du willst und es gestattet ist, so kann ich mir die Dumps einmal anschauen und dann eine grobe Art Anleitung dazu erstellen.
Aber ich muss bei PerfView auch immer wieder "reinkommen", da ich es recht schnell wieder vergesse 😉

Danke für das Angebot, aber das kann ich nicht alleine entscheiden 🙂
Ich spreche das bei nächster Gelegenheit mal an.

Kann aber gut sein, dass sich das um ein paar Wochen (oder sogar Monate) verschiebt, wenn die Notlösung soweit ausreicht, auch da kann ich nur meine Meinung zu sagen, aber nichts entscheiden.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor einem Jahr

Nur, um das Thema hier nicht so halb beendet stehen zu lassen:

Das GC.Collect scheint nur manchmal zu helfen (war ja irgendwie zu erwarten), weiter einzusteigen ist bei dem Zeitdruck aber keine Option.
Der Kunde sagt, es ist "in Ordnung", müsste aber nach dem Release als Erstes angegangen werden.
So ist jetzt auch der Plan, daher lasse ich das erst einmal liegen.

Dein Angebot:

Ich würde das sehr gerne machen, nicht zuletzt, um wieder was dazu zu lernen 😁
Aber wie Du schon vermutet hast, ist das ein sensibles Thema und ich darf dir nichts schicken.
Eventuell ändert sich das nach dem Release nochmal, aber das ist mehr Wunschdenken von mir, als realistisch.
Ich danke dir trotzdem für das Angebot und melde mich, falls ich irgendwann doch darf 🙂

6.910 Beiträge seit 2009
vor einem Jahr

Hallo Palladin007,

danke fürs Update.
Ja schauen wir einmal was / wie es wird. Das solte dann, nicht nur wegen der rechtilchen Bedenken, wohl auch vertraglich sauber geregelt werden, damit alle auf der sicheren Seite sind.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"