다음을 통해 공유


.NET에서 메모리 누수 디버그

이 문서의 적용 대상: ✔️ .NET Core 3.1 SDK 이상 버전

앱이 더 이상 원하는 작업을 수행할 필요가 없는 개체를 참조할 때 메모리가 누수될 수 있습니다. 이러한 개체를 참조하면 가비지 수집기가 사용된 메모리를 회수할 수 없습니다. 이로 인해 성능이 저하되고 예외가 OutOfMemoryException throw될 수 있습니다.

이 자습서에서는 .NET 진단 CLI 도구를 사용하여 .NET 앱에서 메모리 누수를 분석하는 도구를 보여 줍니다. Windows를 사용하는 경우 Visual Studio의 메모리 진단 도구를 사용하여 메모리 누수를 디버그할 수 있습니다.

이 자습서에서는 의도적으로 메모리를 누수하는 샘플 앱을 연습으로 사용합니다. 의도치 않게 메모리를 누수하는 앱을 분석할 수도 있습니다.

이 자습서에서는 다음을 수행합니다.

  • dotnet-counters를 사용하여 관리되는 메모리 사용량을 검사합니다.
  • 덤프 파일을 생성합니다.
  • 덤프 파일을 사용하여 메모리 사용량을 분석합니다.

필수 조건

이 자습서에서는 다음을 사용합니다.

이 자습서에서는 샘플 앱과 도구가 설치되어 있고 사용할 준비가 되었다고 가정합니다.

관리되는 메모리 사용량 검사

이 시나리오의 근본 원인을 찾기 위해 진단 데이터 수집을 시작하기 전에 메모리 누수(메모리 사용량 증가)가 실제로 표시되는지 확인합니다. dotnet-counters 도구를 사용하여 확인할 수 있습니다.

콘솔 창을 열고 샘플 디버그 대상을 다운로드하고 압축을 푼 디렉터리로 이동합니다. 대상을 실행합니다.

dotnet run

별도의 콘솔에서 프로세스 ID를 찾습니다.

dotnet-counters ps

출력은 다음과 유사해야 합니다.

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

비고

이전 명령이 작동하지 않거나 찾을 수 없는 경우 먼저 도구를 설치 dotnet-counters 해야 할 수 있습니다. 다음 명령을 사용합니다.

dotnet tool install --global dotnet-counters

이제 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

이제 인스턴스의 gcroot 명령을 System.String 사용하여 개체가 루팅되는 방법과 이유를 확인할 수 있습니다.

> 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.

StringCustomer 개체에 의해 직접 보유되며, CustomerCache 개체에 의해 간접적으로 보유되는 것을 볼 수 있습니다.

개체를 계속 덤프하여 대부분의 String 개체가 비슷한 패턴을 따르는지 확인할 수 있습니다. 이 시점에서 조사는 코드의 근본 원인을 식별하기에 충분한 정보를 제공했습니다.

이 일반적인 절차를 통해 주요 메모리 누수의 원인을 식별할 수 있습니다.

자원을 정리하세요

이 자습서에서는 샘플 웹 서버를 시작했습니다. 실패한 프로세스 다시 시작 섹션에 설명된 대로 이 서버가 종료되어야 합니다.

만든 덤프 파일을 삭제할 수도 있습니다.

참고하십시오

다음 단계