.NET에서 메모리 누수 디버깅
이 문서의 적용 대상: ✔️ .NET Core 3.1 SDK 이상 버전
앱이 원하는 작업을 수행하는 데 더 이상 필요하지 않은 개체를 참조하면 메모리가 누수될 수 있습니다. 이러한 개체를 참조하면 가비지 수집기가 사용된 메모리를 회수하지 못합니다. 이로 인해 성능이 저하되고 OutOfMemoryException 예외가 throw될 수 있습니다.
이 자습서에서는 .NET 진단 CLI 도구를 사용하여 .NET 앱에서 메모리 누수를 분석하는 도구를 보여 줍니다. Windows를 사용하는 경우 Visual Studio의 메모리 진단 도구를 사용하여 메모리 누수를 디버그할 수 있습니다.
이 자습서에서는 연습으로 의도적으로 메모리 누수를 발생시키는 샘플 앱을 사용합니다. 의도치 않게 메모리 누수를 일으키는 앱을 분석할 수도 있습니다.
이 자습서에서는 다음을 수행합니다.
- dotnet-counters를 사용하여 관리되는 메모리 사용량을 검사합니다.
- 덤프 파일을 생성합니다.
- 덤프 파일을 사용하여 메모리 사용량을 분석합니다.
필수 조건
이 자습서에서는 다음을 사용하여 작업을 수행합니다.
- .NET Core 3.1 SDK 이상 버전.
- 관리되는 메모리 사용량을 검사하는 dotnet-counters.
- 덤프 파일을 수집하고 분석하기 위한 dotnet-dump(SOS 디버깅 확장 포함)
- 진단할 샘플 디버그 대상 앱.
자습서에서는 샘플 앱과 도구가 설치되어 사용할 준비가 되었다고 가정합니다.
관리되는 메모리 사용량 검사
이 시나리오의 근본 원인을 해결하기 위해 진단 데이터 수집을 시작하기 전에 실제로 메모리 누수(메모리 사용량 증가)가 발생하는지 확인합니다. dotnet-counters 도구를 사용하여 이를 확인할 수 있습니다.
콘솔 창을 열고 샘플 디버그 대상을 다운로드하고 압축을 푼 디렉터리로 이동합니다. 대상을 실행합니다.
dotnet run
별도 콘솔에서 프로세스 ID를 찾습니다.
dotnet-counters ps
출력은 다음과 비슷해야 합니다.
4807 DiagnosticScena /home/user/git/samples/core/diagnostics/DiagnosticScenarios/bin/Debug/netcoreapp3.0/DiagnosticScenarios
이제 dotnet-counters 도구를 사용하여 관리되는 메모리 사용량을 검사합니다. --refresh-interval
을 새로 고침 간격(초)을 지정합니다.
dotnet-counters monitor --refresh-interval 1 -p 4807
라이브 출력은 다음과 비슷해야 합니다.
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
이 줄에 집중합니다.
GC Heap Size (MB) 4
시작 직후 관리되는 힙 메모리가 4MB인 것을 알 수 있습니다.
이제 URL https://localhost:5001/api/diagscenario/memleak/20000
으로 이동합니다.
메모리 사용량이 30MB로 증가하는 것을 확인합니다.
GC Heap Size (MB) 30
메모리 사용량을 확인하여 메모리가 증가 또는 감소하는지를 확실히 알 수 있습니다. 다음 단계는 메모리 분석에 적합한 데이터를 수집하는 것입니다.
메모리 덤프 생성
가능한 메모리 누수를 분석할 때 메모리 콘텐츠를 분석하려면 앱의 메모리 힙에 액세스해야 합니다. 개체 간의 관계를 살펴보면 메모리가 해제되지 않는 이유에 대한 이론을 만들 수 있습니다. 일반적인 진단 데이터 원본은 Windows의 메모리 덤프 또는 Linux의 해당 코어 덤프입니다. .NET 애플리케이션의 덤프를 생성하려면 dotnet-dump 도구를 사용할 수 있습니다.
이전에 시작된 샘플 디버그 대상을 사용하여 다음 명령을 실행하고 Linux 코어 덤프를 생성합니다.
dotnet-dump collect -p 4807
그 결과, 동일한 폴더에 코어 덤프가 생성됩니다.
Writing minidump with heap to ./core_20190430_185145
Complete
참고 항목
시간 경과에 따라 비교하기 위해 첫 번째 덤프를 수집한 후 원래 프로세스를 계속 실행하고 동일한 방식으로 두 번째 덤프를 수집합니다. 그러면 메모리 사용량이 증가하는 위치를 확인하기 위해 비교할 수 있는 일정 기간 동안 두 개의 덤프가 생성됩니다.
실패한 프로세스 다시 시작
덤프가 수집되면 실패한 프로세스를 진단하는 데 충분한 정보가 확보될 것입니다. 실패한 프로세스가 프로덕션 서버에서 실행 중이라면 프로세스를 다시 시작하여 단기 수정하기에 가장 적합한 때입니다.
이 자습서에서는 샘플 디버그 대상이 완료되었으므로 이를 종료할 수 있습니다. 서버를 시작한 터미널로 이동하고 Ctrl+C를 누릅니다.
코어 덤프 분석
이제 코어 덤프가 생성되었으므로 dotnet-dump 도구를 사용하여 덤프를 분석합니다.
dotnet-dump analyze core_20190430_185145
여기서 core_20190430_185145
는 분석하려는 코어 덤프의 이름입니다.
참고 항목
libdl.so를 찾을 수 없다는 오류가 표시되는 경우 libc6-dev 패키지를 설치해야 할 수 있습니다. 자세한 내용은 Linux의 .NET 필수 조건을 참조하세요.
SOS 명령을 입력할 수 있는 프롬프트가 표시됩니다. 일반적으로는 관리되는 힙의 전반적인 상태를 확인하는 것이 가장 좋습니다.
> 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
여기에서 대부분의 개체는 String
또는 Customer
개체임을 확인할 수 있습니다.
MT(메서드 테이블)와 함께 dumpheap
명령을 사용하여 모든 String
인스턴스의 목록을 가져올 수 있습니다.
> 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
이제 System.String
인스턴스에 gcroot
명령을 사용하여 개체가 루팅된 방법과 이유를 확인할 수 있습니다.
> 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.
Customer
개체에서 직접적으로, CustomerCache
개체에서 간접적으로 String
을 보유하는 것을 알 수 있습니다.
개체를 계속 덤프하여 대부분의 String
개체가 비슷한 패턴을 따르는지 확인할 수 있습니다. 이 시점에서 조사를 통해 코드에서 근본 원인을 식별하는 데 충분한 정보가 제공됩니다.
이 일반적인 절차를 사용하면 주요 메모리 누수의 원인을 식별할 수 있습니다.
리소스 정리
이 자습서에서 샘플 웹 서버를 시작했습니다. 이 서버는 실패한 프로세스 다시 시작 섹션에서 설명한 것과 같이 종료되어 있어야 합니다.
또한 생성된 덤프 파일을 삭제할 수 있습니다.
참고 항목
- 프로세스를 나열하는 dotnet-trace
- 관리되는 메모리 사용량을 검사하는 dotnet-counters
- 덤프 파일을 수집 및 분석하는 dotnet-dump
- dotnet/diagnostics
- Visual Studio를 사용하여 메모리 누수 디버그
다음 단계
.NET