Debugowanie przecieku pamięci na platformie .NET
Ten artykuł dotyczy: ✔️ zestaw .NET Core 3.1 SDK i nowsze wersje
Pamięć może wyciekać, gdy aplikacja odwołuje się do obiektów, których nie potrzebuje już do wykonania żądanego zadania. Odwoływanie się do tych obiektów zapobiega odzyskiwaniu używanej pamięci przez moduł odśmiecania pamięci. Może to spowodować obniżenie wydajności i OutOfMemoryException zgłoszony wyjątek.
W tym samouczku przedstawiono narzędzia do analizowania przecieku pamięci w aplikacji platformy .NET przy użyciu narzędzi interfejsu wiersza polecenia diagnostyki platformy .NET. Jeśli korzystasz z systemu Windows, możesz użyć narzędzi diagnostycznych pamięci programu Visual Studio do debugowania przecieku pamięci.
W tym samouczku użyto przykładowej aplikacji, która celowo przecieka pamięć w ramach ćwiczenia. Możesz również analizować aplikacje, które przypadkowo przeciekają pamięć.
Ten samouczek obejmuje następujące kroki:
- Sprawdź użycie pamięci zarządzanej za pomocą liczników dotnet-counter.
- Wygeneruj plik zrzutu.
- Przeanalizuj użycie pamięci przy użyciu pliku zrzutu.
Wymagania wstępne
W tym samouczku są używane następujące elementy:
- Zestaw .NET Core 3.1 SDK lub nowsza wersja.
- dotnet-counters do sprawdzania użycia pamięci zarządzanej.
- dotnet-dump do zbierania i analizowania pliku zrzutu (łącznie z rozszerzeniem debugowania SOS).
- Przykładowa aplikacja docelowa debugowania do diagnozowania.
W tym samouczku założono, że przykładowe aplikacje i narzędzia są instalowane i gotowe do użycia.
Badanie użycia pamięci zarządzanej
Przed rozpoczęciem zbierania danych diagnostycznych w celu ułatwienia głównej przyczyny tego scenariusza upewnij się, że w rzeczywistości występuje wyciek pamięci (wzrost użycia pamięci). Aby to potwierdzić, możesz użyć narzędzia dotnet-counters .
Otwórz okno konsoli i przejdź do katalogu, w którym pobrano i rozpakujesz przykładowy element docelowy debugowania. Uruchom element docelowy:
dotnet run
Z oddzielnej konsoli znajdź identyfikator procesu:
dotnet-counters ps
Dane wyjściowe powinny być podobne do następujących:
4807 DiagnosticScena /home/user/git/samples/core/diagnostics/DiagnosticScenarios/bin/Debug/netcoreapp3.0/DiagnosticScenarios
Teraz sprawdź użycie pamięci zarządzanej za pomocą narzędzia dotnet-counters . Parametr --refresh-interval
określa liczbę sekund między odświeżeniami:
dotnet-counters monitor --refresh-interval 1 -p 4807
Dane wyjściowe na żywo powinny być podobne do następujących:
Press p to pause, r to resume, q to quit.
Status: Running
[System.Runtime]
# of Assemblies Loaded 118
% Time in GC (since last GC) 0
Allocation Rate (Bytes / sec) 37,896
CPU Usage (%) 0
Exceptions / sec 0
GC Heap Size (MB) 4
Gen 0 GC / sec 0
Gen 0 Size (B) 0
Gen 1 GC / sec 0
Gen 1 Size (B) 0
Gen 2 GC / sec 0
Gen 2 Size (B) 0
LOH Size (B) 0
Monitor Lock Contention Count / sec 0
Number of Active Timers 1
ThreadPool Completed Work Items / sec 10
ThreadPool Queue Length 0
ThreadPool Threads Count 1
Working Set (MB) 83
Skupienie się na tym wierszu:
GC Heap Size (MB) 4
Widać, że zarządzana pamięć sterta wynosi 4 MB bezpośrednio po uruchomieniu.
Teraz przejdź do adresu URL https://localhost:5001/api/diagscenario/memleak/20000
.
Zwróć uwagę, że użycie pamięci wzrosła do 30 MB.
GC Heap Size (MB) 30
Obserwując użycie pamięci, można bezpiecznie powiedzieć, że pamięć rośnie lub przecieka. Następnym krokiem jest zebranie odpowiednich danych na potrzeby analizy pamięci.
Generowanie zrzutu pamięci
Podczas analizowania możliwych przecieków pamięci potrzebny jest dostęp do sterta pamięci aplikacji, aby przeanalizować zawartość pamięci. Patrząc na relacje między obiektami, można tworzyć teorie, dlaczego pamięć nie jest zwalniana. Typowym źródłem danych diagnostycznych jest zrzut pamięci w systemie Windows lub równoważny zrzut rdzeni w systemie Linux. Aby wygenerować zrzut aplikacji .NET, możesz użyć narzędzia dotnet-dump .
Korzystając z przykładowego celu debugowania uruchomionego wcześniej, uruchom następujące polecenie, aby wygenerować podstawowy zrzut systemu Linux:
dotnet-dump collect -p 4807
Wynikiem jest podstawowy zrzut znajdujący się w tym samym folderze.
Writing minidump with heap to ./core_20190430_185145
Complete
Uwaga
W przypadku porównania z upływem czasu niech oryginalny proces będzie kontynuowany po zebraniu pierwszego zrzutu i zebraniu drugiego zrzutu w taki sam sposób. Następnie w danym okresie można porównać dwa zrzuty, aby zobaczyć, gdzie rośnie użycie pamięci.
Uruchom ponownie proces, który zakończył się niepowodzeniem
Po zebraniu zrzutu należy mieć wystarczające informacje, aby zdiagnozować proces, który zakończył się niepowodzeniem. Jeśli proces, który zakończył się niepowodzeniem, jest uruchomiony na serwerze produkcyjnym, teraz jest to idealny czas na krótkoterminowe korygowanie przez ponowne uruchomienie procesu.
W tym samouczku wykonano teraz elementy docelowe debugowania przykładowego i możesz je zamknąć. Przejdź do terminalu, który uruchomił serwer, i naciśnij klawisze Ctrl+C.
Analizowanie zrzutu rdzenia
Po wygenerowaniu podstawowego zrzutu użyj narzędzia dotnet-dump , aby przeanalizować zrzut:
dotnet-dump analyze core_20190430_185145
Gdzie core_20190430_185145
to nazwa podstawowego zrzutu, który chcesz przeanalizować.
Uwaga
Jeśli zostanie wyświetlony błąd zgłaszający błąd, że nie można odnaleźć libdl.so , może być konieczne zainstalowanie pakietu libc6-dev . Aby uzyskać więcej informacji, zobacz Wymagania wstępne dotyczące platformy .NET w systemie Linux.
Zostanie wyświetlony monit, w którym można wprowadzić polecenia SOS . Często pierwszą rzeczą, którą chcesz przyjrzeć, jest ogólny stan zarządzanej sterty:
> 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
W tym miejscu widać, że większość obiektów jest albo String
Customer
obiektami.
Możesz ponownie użyć dumpheap
polecenia z tabelą metod (MT), aby uzyskać listę wszystkich String
wystąpień:
> 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
Teraz możesz użyć polecenia w wystąpieniu gcroot
System.String
, aby zobaczyć, jak i dlaczego obiekt jest zakorzeniony:
> 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.
Widać, że obiekt String
jest przechowywany bezpośrednio przez Customer
obiekt i pośrednio przechowywany przez CustomerCache
obiekt.
Możesz nadal usuwać obiekty, aby zobaczyć, że większość String
obiektów postępuje zgodnie z podobnym wzorcem. W tym momencie badanie dostarczyło wystarczające informacje, aby zidentyfikować główną przyczynę w kodzie.
Ta ogólna procedura umożliwia zidentyfikowanie źródła poważnych przecieków pamięci.
Czyszczenie zasobów
W tym samouczku uruchomiono przykładowy serwer internetowy. Ten serwer powinien zostać zamknięty zgodnie z wyjaśnieniem w sekcji Ponowne uruchamianie procesu , który zakończył się niepowodzeniem.
Możesz również usunąć utworzony plik zrzutu.
Zobacz też
- dotnet-trace do list procesów
- dotnet-counters do sprawdzania użycia pamięci zarządzanej
- dotnet-dump do zbierania i analizowania pliku zrzutu
- dotnet/diagnostics
- Debugowanie przecieków pamięci przy użyciu programu Visual Studio