Freigeben über


Debuggen eines Speicherverlusts in .NET

Dieser Artikel gilt für: ✔️ .NET Core 3.1 SDK und höher

Speicher kann verloren gehen, wenn Ihre App auf Objekte verweist, die nicht mehr die gewünschte Aufgabe ausführen müssen. Durch Verweisen auf diese Objekte wird verhindert, dass der Garbage Collector den verwendeten Arbeitsspeicher zurückgibt. Dies kann zu Leistungsbeeinträchtigungen und zum Auslösen einer OutOfMemoryException-Ausnahme führen.

In diesem Lernprogramm werden die Tools zum Analysieren eines Speicherverlusts in einer .NET-App mithilfe der .NET Diagnostics CLI-Tools veranschaulicht. Wenn Sie Windows verwenden, können Sie möglicherweise die Speicherdiagnosetools von Visual Studio verwenden , um den Speicherverlust zu debuggen.

In diesem Tutorial wird als Übung eine Beispiel-App verwendet, bei der absichtlich Arbeitsspeicher verloren geht. Sie können auch Apps analysieren, die unbeabsichtigt Speicherlecks verursachen.

In diesem Tutorial werden Sie Folgendes lernen:

  • Untersuchen Sie den verwalteten Speicherverbrauch mit dotnet-counters.
  • Generieren einer Dumpdatei.
  • Analysieren Sie die Speicherauslastung mithilfe der Speicherabbilddatei.

Voraussetzungen

Das Tutorial verwendet:

Im Lernprogramm wird davon ausgegangen, dass die Beispiel-Apps und -Tools installiert und einsatzbereit sind.

Wenn Ihre App eine version von .NET ausführt, die älter als .NET 9 ist, sieht die Ausgabebenutzeroberfläche von dotnet-counters etwas anders aus. Weitere Informationen finden Sie unter dotnet-counters .

Überprüfen der Verwalteten Speicherauslastung

Bevor Sie mit dem Erfassen von Diagnosedaten beginnen, um die Grundursache für dieses Szenario zu ermitteln, stellen Sie sicher, dass tatsächlich ein Arbeitsspeicherverlust stattfindet (Steigerung der Arbeitsspeichernutzung). Sie können das Tool dotnet-counters verwenden, um dies zu bestätigen.

Öffnen Sie ein Konsolenfenster, und navigieren Sie zu dem Verzeichnis, in das Sie das Beispieldebugziel heruntergeladen und entzippt haben. Führen Sie das Ziel aus:

dotnet run

Suchen Sie in einer separaten Konsole die Prozess-ID:

dotnet-counters ps

Die Ausgabe sollte in etwa wie folgt aussehen:

4807 DiagnosticScena /home/user/git/samples/core/diagnostics/DiagnosticScenarios/bin/Debug/netcoreapp3.0/DiagnosticScenarios

Hinweis

Wenn der vorherige Befehl nicht funktioniert oder nicht gefunden wird, müssen Sie das dotnet-counters Tool wahrscheinlich zuerst installieren. Verwenden Sie den folgenden Befehl:

dotnet tool install --global dotnet-counters

Überprüfen Sie nun die verwaltete Speicherauslastung mit dem Dotnet-Counter-Tool . Dies --refresh-interval gibt die Anzahl der Sekunden zwischen Aktualisierungen an:

dotnet-counters monitor --refresh-interval 1 -p 4807

Die Liveausgabe sollte ähnlich wie die folgenden sein:

Press p to pause, r to resume, q to quit.
Status: Running

Name                                                            Current Value
[System.Runtime]
    dotnet.assembly.count ({assembly})                              111
    dotnet.gc.collections ({collection})
    gc.heap.generation
    ------------------
    gen0                                                              1
    gen1                                                              0
    gen2                                                              0
    dotnet.gc.heap.total_allocated (By)                       4,431,712
    dotnet.gc.last_collection.heap.fragmentation.size (By)
    gc.heap.generation
    ------------------
    gen0                                                        803,576
    gen1                                                         15,456
    gen2                                                              0
    loh                                                               0
    poh                                                               0
    dotnet.gc.last_collection.heap.size (By)
    gc.heap.generation
    ------------------
    gen0                                                        811,960
    gen1                                                      1,214,720
    gen2                                                              0
    loh                                                               0
    poh                                                          24,528
    dotnet.gc.last_collection.memory.committed_size (By)      4,296,704
    dotnet.gc.pause.time (s)                                          0.003
    dotnet.jit.compilation.time (s)                                   0.329
    dotnet.jit.compiled_il.size (By)                            120,212
    dotnet.jit.compiled_methods ({method})                            1,202
    dotnet.monitor.lock_contentions ({contention})                    2
    dotnet.process.cpu.count ({cpu})                                 22
    dotnet.process.cpu.time (s)
    cpu.mode
    --------
    system                                                            0.344
    user                                                              0.344
    dotnet.process.memory.working_set (By)                   64,331,776
    dotnet.thread_pool.queue.length ({work_item})                     0
    dotnet.thread_pool.thread.count ({thread})                        0
    dotnet.thread_pool.work_item.count ({work_item})                  7
    dotnet.timer.count ({timer})                                      0

Konzentrieren Sie sich auf diese Zeile:

    dotnet.gc.last_collection.memory.committed_size (By)   4,296,704

Sie können sehen, dass der verwaltete Heap-Speicher direkt nach dem Start 4 MB beträgt.

Wechseln Sie nun zur URL https://localhost:5001/api/diagscenario/memleak/20000.

Beachten Sie, dass die Speicherauslastung auf über 20 MB gestiegen ist.

    dotnet.gc.last_collection.memory.committed_size (By)   21,020,672

Indem Sie die Speicherauslastung beobachten, können Sie sicher sagen, dass der Speicher wächst oder verloren geht. Der nächste Schritt besteht darin, die richtigen Daten für die Speicheranalyse zu sammeln.

Generieren eines Speicherabbilds

Bei der Analyse möglicher Speicherverluste benötigen Sie Zugriff auf den Speicher-Heap der App, um den Speicherinhalt zu analysieren. Wenn Sie beziehungen zwischen Objekten betrachten, erstellen Sie Theorien darüber, warum der Speicher nicht freigegeben wird. Eine allgemeine Diagnosedatenquelle ist ein Speicherabbild unter Windows oder das entsprechende Kernabbild unter Linux. Zum Generieren eines Dumps einer . NET-Anwendung können Sie das dotnet-dump-Tool verwenden.

Führen Sie mit dem zuvor gestarteten Beispiel-Debug-Ziel den folgenden Befehl aus, um einen Linux-Core-Dump zu erstellen.

dotnet-dump collect -p 4807

Das Ergebnis ist ein Core Dump, der sich im gleichen Ordner befindet.

Writing minidump with heap to ./core_20190430_185145
Complete

Hinweis

Lassen Sie für einen Vergleich im Laufe der Zeit den ursprünglichen Prozess nach dem Sammeln des ersten Speicherabbilds weiter ausführen, und sammeln Sie ein zweites Speicherabbild auf die gleiche Weise. Sie würden dann über zwei Speicherabbilder über einen Zeitraum verfügen, die Sie vergleichen können, um zu sehen, wo die Speicherauslastung wächst.

Neustart des fehlgeschlagenen Prozesses

Nachdem der Dump erfasst ist, sollten Sie über ausreichende Informationen verfügen, um den fehlgeschlagenen Prozess zu diagnostizieren. Wenn der fehlgeschlagene Prozess auf einem Produktionsserver ausgeführt wird, ist es jetzt die ideale Zeit für kurzfristige Korrekturen, indem der Prozess neu gestartet wird.

In diesem Tutorial benötigen Sie das Beispieldebugziel nicht mehr, und Sie können es schließen. Navigieren Sie zum Terminal, das den Server gestartet hat, und drücken Sie STRG+C.

Analysieren des Speicherabbilds

Nachdem Sie nun ein Speicherabbild generiert haben, verwenden Sie das Tool dotnet-dump, um das Speicherabbild zu analysieren:

dotnet-dump analyze core_20190430_185145

Dabei ist core_20190430_185145 der Name des Speicherabbilds, das Sie analysieren möchten.

Hinweis

Wenn ein Fehler angezeigt wird, der beschwert, dass libdl.so nicht gefunden werden kann, müssen Sie möglicherweise das libc6-Dev-Paket installieren. Weitere Informationen finden Sie unter Voraussetzungen für .NET unter Linux.

Sie erhalten eine Eingabeaufforderung, an der Sie SOS-Befehle eingeben können. Im Allgemeinen gilt die erste Untersuchung dem Gesamtstatus des verwalteten Heaps:

> dumpheap -stat

Statistics:
              MT    Count    TotalSize Class Name
...
00007f6c1eeefba8      576        59904 System.Reflection.RuntimeMethodInfo
00007f6c1dc021c8     1749        95696 System.SByte[]
00000000008c9db0     3847       116080      Free
00007f6c1e784a18      175       128640 System.Char[]
00007f6c1dbf5510      217       133504 System.Object[]
00007f6c1dc014c0      467       416464 System.Byte[]
00007f6c21625038        6      4063376 testwebapi.Controllers.Customer[]
00007f6c20a67498   200000      4800000 testwebapi.Controllers.Customer
00007f6c1dc00f90   206770     19494060 System.String
Total 428516 objects

Hier können Sie sehen, dass die meisten Objekte entweder String oder Customer Objekte sind.

Sie können den dumpheap Befehl erneut mit der Methodentabelle (MT) verwenden, um eine Liste aller String Instanzen abzurufen:

> dumpheap -mt 00007f6c1dc00f90

         Address               MT     Size
...
00007f6ad09421f8 00007faddaa50f90       94
...
00007f6ad0965b20 00007f6c1dc00f90       80
00007f6ad0965c10 00007f6c1dc00f90       80
00007f6ad0965d00 00007f6c1dc00f90       80
00007f6ad0965df0 00007f6c1dc00f90       80
00007f6ad0965ee0 00007f6c1dc00f90       80

Statistics:
              MT    Count    TotalSize Class Name
00007f6c1dc00f90   206770     19494060 System.String
Total 206770 objects

Sie können nun den gcroot Befehl für eine System.String Instanz verwenden, um zu sehen, wie und warum das Objekt verwurzelt ist:

> gcroot 00007f6ad09421f8

Thread 3f68:
    00007F6795BB58A0 00007F6C1D7D0745 System.Diagnostics.Tracing.CounterGroup.PollForValues() [/_/src/System.Private.CoreLib/shared/System/Diagnostics/Tracing/CounterGroup.cs @ 260]
        rbx:  (interior)
            ->  00007F6BDFFFF038 System.Object[]
            ->  00007F69D0033570 testwebapi.Controllers.Processor
            ->  00007F69D0033588 testwebapi.Controllers.CustomerCache
            ->  00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer, DiagnosticScenarios]]
            ->  00007F6C000148A0 testwebapi.Controllers.Customer[]
            ->  00007F6AD0942258 testwebapi.Controllers.Customer
            ->  00007F6AD09421F8 System.String

HandleTable:
    00007F6C98BB15F8 (pinned handle)
    -> 00007F6BDFFFF038 System.Object[]
    -> 00007F69D0033570 testwebapi.Controllers.Processor
    -> 00007F69D0033588 testwebapi.Controllers.CustomerCache
    -> 00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer, DiagnosticScenarios]]
    -> 00007F6C000148A0 testwebapi.Controllers.Customer[]
    -> 00007F6AD0942258 testwebapi.Controllers.Customer
    -> 00007F6AD09421F8 System.String

Found 2 roots.

Man sieht, dass das String-Objekt direkt vom Customer-Objekt gehalten wird und indirekt von einem CustomerCache-Objekt.

Sie können weiterhin Objekte verwerfen, um zu sehen, dass die meisten String Objekte einem ähnlichen Muster folgen. An diesem Punkt lieferte die Untersuchung ausreichende Informationen, um die Ursache in Ihrem Code zu identifizieren.

Mit diesem allgemeinen Verfahren können Sie die Quelle wichtiger Speicherverluste identifizieren.

Bereinigen von Ressourcen

In diesem Lernprogramm haben Sie einen Beispielwebserver gestartet. Dieser Server sollte heruntergefahren worden sein, wie im Abschnitt " Fehler beim Prozess neu starten " erläutert wurde.

Sie können auch die erstellte Dumpdatei löschen.

Siehe auch

Nächste Schritte