다음을 통해 공유


가비지 수집 및 성능

이 항목에서는 가비지 수집 및 메모리 사용과 관련된 문제에 대해 설명합니다. 관리되는 힙과 관련된 문제를 해결하고 가비지 수집이 응용 프로그램에 미치는 영향을 최소화하는 방법에 대해 설명합니다. 각 문제에는 문제 조사를 위해 사용할 수 있는 절차에 대한 링크가 포함되어 있습니다.

이 항목에는 다음과 같은 단원이 포함되어 있습니다.

  • 성능 분석 도구

  • 성능 문제 해결

  • 문제 해결 지침

  • 성능 검사 절차

성능 분석 도구

다음 단원에서는 메모리 사용 및 가비지 수집 문제를 조사할 때 사용할 수 있는 도구에 대해 설명합니다. 이러한 도구는 이 항목의 뒷부분에 제공된 절차에서 사용됩니다.

메모리 성능 카운터

성능 카운터를 사용하여 성능 데이터를 수집할 수 있습니다. 자세한 내용은 런타임 프로파일링를 참조하십시오. 메모리 성능 카운터의 설명에서와 같이 성능 카운터의 .NET CLR Memory 범주는 가비지 수집기에 대한 정보를 제공합니다.

SOS 디버거

Windows 디버거(WinDbg) 또는 SOS.dll(SOS 디버깅 확장)을 포함한 Visual Studio 디버거를 사용하여 관리되는 힙의 개체를 조사할 수 있습니다.

WinDbg를 설치하려면 WDK 및 개발자 도구 웹 사이트에서 Windows용 디버깅 도구를 설치합니다. SOS 디버깅 확장을 사용하는 방법은 방법: SOS 사용을 참조하십시오.

가비지 수집 ETW 이벤트

ETW(Windows용 이벤트 추적)는 .NET Framework에서 제공되는 프로파일링 및 디버깅 지원을 보완하는 추적 시스템입니다. .NET Framework 버전 4부터 가비지 수집 ETW 이벤트는 통계적인 관점에서 관리되는 힙을 분석하는 데 유용한 정보를 캡처합니다. 예를 들어 가비지 수집이 발생할 가능성이 있을 때 실행되는 GCStart_V1 이벤트는 다음과 같은 정보를 제공합니다.

  • 수집 중인 개체의 세대

  • 가비지 수집을 트리거한 개체

  • 가비지 수집 유형(동시 또는 비동시)

ETW 이벤트 로깅은 효율적이며 가비지 수집과 연관된 성능 문제를 숨기지 않습니다. 프로세스는 ETW 이벤트와 함께 자신의 고유 이벤트를 제공할 수 있습니다. 이벤트가 로깅되면 응용 프로그램의 이벤트와 가비지 수집 이벤트를 연결하여 힙 문제의 발생 방식 및 시간을 확인할 수 있습니다. 예를 들어 서버 응용 프로그램은 클라이언트 요청의 시작과 끝에 이벤트를 제공할 수 있습니다.

프로파일링 API

CLR(공용 언어 런타임) 프로파일링 인터페이스는 가비지 수집 중에 영향을 받은 개체에 대한 자세한 정보를 제공합니다. 가비지 수집이 시작되고 끝나면 이를 프로파일러에게 알립니다. 프로파일링 API는 각 세대에서 개체의 ID를 포함하여 관리되는 힙의 개체에 대한 보고서를 제공할 수 있습니다. 자세한 내용은 프로파일링 API의 개체 추적을 참조하십시오.

프로파일러는 광범위한 정보를 제공할 수 있습니다. 하지만 복잡한 프로파일러는 잠재적으로 응용 프로그램의 동작을 변경할 수 있습니다.

응용 프로그램 도메인 리소스 모니터링

.NET Framework 4부터 ARM(응용 프로그램 도메인 리소스 모니터링)을 사용하면 호스트에서 CPU와 메모리 사용을 응용 프로그램 도메인별로 모니터링할 수 있습니다. 자세한 내용은 응용 프로그램 도메인 리소스 모니터링을 참조하십시오.

맨 위로 이동

성능 문제 해결

첫 번째 단계는 문제가 실제로 가비지 수집 문제인지 확인하는 것입니다. 실제로 가비지 수집 문제인 것으로 확인될 경우 문제 해결을 위해 다음 목록 중에서 선택합니다.

  • 메모리 부족 예외가 throw됨

  • 프로세스에 메모리가 너무 많이 사용됨

  • 가비지 수집기가 개체를 회수하는 속도가 충분히 빠르지 않음

  • 관리되는 힙이 너무 많이 조각화됨

  • 가비지 수집이 너무 오랫동안 일시 중지됨

  • 0세대가 너무 큼

  • 가비지 수집 중 CPU 사용량이 너무 높음

문제: 메모리 부족 예외가 throw됨

관리되는 OutOfMemoryException이 정상적으로 throw되는 두 가지 경우는 다음과 같습니다.

  • 가상 메모리 부족

    가비지 수집기는 시스템에서 미리 결정된 크기의 세그먼트로 메모리를 할당합니다. 할당 시 추가 세그먼트가 필요하지만 프로세스의 가상 메모리 공간에 비어 있는 연속된 블록이 남아 있지 않으면 관리되는 힙에 대한 할당이 실패합니다.

  • 할당할 수 있는 실제 메모리 부족

성능 검사

메모리 부족 예외가 관리되는 예외인지 확인합니다.

예약할 수 있는 가상 메모리 양을 확인합니다.

실제 메모리가 충분한지 확인합니다.

예외가 정상적인 예외가 아닌 것으로 확인될 경우 Microsoft 고객 서비스 지원 센터에 다음 정보를 제공하고 문의하십시오.

  • 관리되는 메모리 부족 예외가 포함된 스택

  • 전체 메모리 덤프

  • 해당 예외가 정상적인 메모리 부족 예외가 아님을 입증하는 데이터(가상 메모리 또는 실제 메모리가 문제의 원인이 아님을 보여 주는 데이터 포함)

문제: 프로세스에 메모리가 너무 많이 사용됨

이 문제와 관련해서는 일반적으로 Windows 작업 관리자의 성능 탭에 있는 메모리 사용량 표시에 메모리 사용량이 너무 많은 것으로 표시된다고 가정해 볼 수 있습니다. 하지만 이 수치는 작업 집합과 관련된 것이며, 가상 메모리 사용에 대한 정보를 제공하지 않습니다.

해당 문제가 관리되는 힙으로 인한 것으로 확인될 경우 시간을 두고 관리되는 힙을 측정하여 패턴을 확인해야 합니다.

해당 문제가 관리되는 힙으로 인한 것이 아니라고 확인될 경우에는 네이티브 디버깅을 사용해야 합니다.

성능 검사

예약할 수 있는 가상 메모리 양을 확인합니다.

관리되는 힙이 커밋 중인 메모리 양을 확인합니다.

관리되는 힙이 예약한 메모리 양을 확인합니다.

2세대에서 대형 개체를 확인합니다.

개체에 대한 참조를 확인합니다.

문제: 가비지 수집기가 개체를 회수하는 속도가 충분히 빠르지 않음

가비지 수집에서 개체가 예상한 대로 회수되지 않는 것으로 보일 경우 이러한 개체에 대한 강력한 참조가 있는지 확인해야 합니다.

비활성 개체가 포함된 세대에 가비지 수집이 수행되지 않은 경우, 즉 비활성 개체에 대한 종료자가 실행되지 않은 경우에도 이 문제가 발견될 수 있습니다. 예를 들어 STA(단일 스레드 아파트) 응용 프로그램을 실행 중이고 종료자 큐를 제공하는 스레드가 이 응용 프로그램을 호출할 수 없는 경우에 이 문제가 발생할 수 있습니다.

성능 검사

개체에 대한 참조를 확인합니다.

종료자가 실행되었는지 여부를 확인합니다.

종료하려고 대기 중인 개체가 있는지 확인합니다.

문제: 관리되는 힙이 너무 많이 조각화됨

조각화 수준은 해당 세대에 할당된 총 메모리에서 여유 공간의 비율로 계산됩니다. 2세대의 경우 허용 가능한 조각화 수준은 20% 이하입니다. 2세대는 크기가 매우 커질 수 있기 때문에 조각화 비율이 절대 값보다 더 중요합니다.

0세대는 새 개체가 할당되는 세대이므로 0세대에서는 여유 공간이 많더라도 문제가 되지 않습니다.

개체 힙은 압축되지 않기 때문에 조각화는 항상 대형 개체 힙에서 발생합니다. 인접한 비어 있는 개체는 대형 개체 할당 요청을 충족할 수 있도록 자연스럽게 단일 공간으로 축소됩니다.

조각화는 1세대와 2세대에서 문제가 될 수 있습니다. 가비지 수집 후 이러한 세대에 대량의 여유 공간이 포함되었고, 응용 프로그램의 개체 사용을 수정해야 할 경우 장기 개체의 수명을 다시 평가해야 합니다.

개체를 과도하게 고정하면 조각화가 늘어날 수 있습니다. 조각화 수준이 높은 경우 너무 많은 개체가 고정되었을 수 있습니다.

가상 메모리 조각화로 인해 가비지 수집기가 세그먼트를 추가할 수 없는 경우 이에 대한 가능한 원인은 다음 중 하나일 수 있습니다.

  • 많은 수의 소형 어셈블리에 대한 잦은 로딩 및 언로딩

  • 비관리 코드와의 상호 작용 시 COM 개체에 대해 너무 많은 참조 유지

  • 대형 개체 힙에서 힙 세그먼트 할당과 해제를 자주 일으키는 대형 임시 개체 생성

    CLR을 호스팅할 때 응용 프로그램은 가비지 수집기가 해당 세그먼트를 보존하도록 요청할 수 있습니다. 그 결과 세그먼트 할당이 자주 발생합니다. 이 작업은 STARTUP_FLAGS 열거형에서 STARTUP_HOARD_GC_VM 플래그를 사용하여 수행됩니다.

성능 검사

관리되는 힙에서 여유 공간의 양을 확인합니다.

고정된 개체의 수를 확인합니다.

조각화가 발생할 정상적인 원인이 없다고 판단될 경우 Microsoft 고객 서비스 지원 센터에 문의하십시오.

문제: 가비지 수집이 너무 오랫동안 일시 중지됨

가비지 수집은 연성 실시간으로 작동하므로 응용 프로그램에서 약간의 일시 중지는 허용될 수 있습니다. 연성 실시간의 조건으로 작업의 95%는 시간 내에 완료되어야 합니다.

동시 가비지 수집의 경우 수집 중 관리되는 스레드 실행이 허용되므로 일시 중지 시간이 매우 작습니다.

임시 가비지 수집(0세대 및 1세대)은 수 밀리초 정도만 지속되므로 일시 중지 시간을 줄이는 방법은 일반적으로 가능하지 않습니다. 하지만 2세대 수집에서는 응용 프로그램의 할당 요청 패턴을 변경하여 일시 중지 시간을 줄일 수 있습니다.

또 다른 보다 정확한 방법은 가비지 수집 ETW 이벤트를 사용하는 것입니다. 이벤트 시퀀스에 대해 타임스탬프 차등값을 추가하여 수집에 대한 타이밍을 찾을 수 있습니다. 전체 수집 시퀀스에는 실행 엔진의 일시 중단, 가비지 수집 자체 및 실행 엔진의 다시 시작이 포함됩니다.

가비지 수집 알림을 사용하여 서버가 2세대 수집을 포함할지 여부 및 다른 서버로의 다시 라우팅 요청이 일시 중지 문제 해결에 도움이 되는지 여부를 확인할 수 있습니다.

성능 검사

가비지 수집의 시간 길이를 확인합니다.

가비지 수집을 일으킨 항목을 확인합니다.

문제: 0세대가 너무 큼

64비트 시스템에서 특히 워크스테이션 가비지 수집 대신 서버 가비지 수집을 사용할 경우에는 0세대에 대량의 개체 수가 포함될 수 있습니다. 그 이유는 이러한 환경에서 0세대 가비지 수집을 트리거하는 임계값이 클수록 0세대 수집이 더 커질 수 있기 때문입니다. 가비지 수집이 트리거되기 전에 응용 프로그램에서 더 많은 메모리를 할당하면 성능이 향상됩니다.

문제: 가비지 수집 중 CPU 사용량이 너무 높음

가비지 수집 중에는 CPU 사용량이 높아집니다. 처리 시간의 상당 부분이 가비지 수집에 소비되는 이유는 수집이 너무 자주 수행되거나 수집이 너무 오래 지속되기 때문입니다. 관리되는 힙에서 개체의 할당 비율이 늘어나면 가비지 수집이 너무 자주 수행됩니다. 할당 비율을 낮추면 가비지 수집 빈도가 줄어듭니다.

할당 비율은 Allocated Bytes/second 성능 카운터를 사용하여 모니터링할 수 있습니다. 자세한 내용은 메모리 성능 카운터를 참조하십시오.

수집 기간은 주로 할당 후 지속되는 개체 수를 나타내는 요소입니다. 가비지 수집기는 수집해야 할 개체가 많이 남아 있는 경우 대량의 메모리를 사용해야 합니다. 남은 개체를 압축하는 작업은 시간이 오래 걸립니다. 수집 중 처리된 개체 수를 확인하려면 디버거에서 지정된 세대의 가비지 수집 끝에 중단점을 설정합니다.

성능 검사

높은 CPU 사용량이 가비지 수집으로 인한 것인지 확인합니다.

가비지 수집의 끝에 중단점을 설정합니다.

맨 위로 이동

문제 해결 지침

이 단원에서는 조사를 시작할 때 고려해야 하는 지침에 대해 설명합니다.

워크스테이션 또는 서버 가비지 수집

올바른 유형의 가비지 수집을 사용 중인지 확인합니다. 응용 프로그램에서 여러 개의 스레드와 개체 인스턴스를 사용하는 경우 워크스테이션 가비지 수집 대신 서버 가비지 수집을 사용합니다. 서버 가비지 수집은 여러 스레드에서 작동하지만 워크스테이션 가비지 수집을 사용하려면 한 응용 프로그램의 여러 인스턴스가 각자 고유의 가비지 수집 스레드를 실행해야 하므로 CPU 시간 경합이 발생합니다.

서비스와 같이 부하가 낮고 백그라운드에서 작업을 이따금씩 수행하는 응용 프로그램은 동시 가비지 수집을 비활성화한 상태로 워크스테이션 가비지 수집을 사용할 수 있습니다.

관리되는 힙 크기를 측정해야 하는 경우

프로파일러를 사용하지 않는 한 성능 문제를 효율적으로 진단하기 위해 일관적인 측정 패턴을 설정해야 합니다. 일정을 설정할 때는 다음과 같은 요소를 고려하십시오.

  • 2세대 가비지 수집 후 측정하면 전체 관리되는 힙에 가비지가 남아 있지 않습니다(비활성 개체).

  • 0세대 가비지 수집 직후에 측정하면 1세대 및 2세대의 개체가 아직 수집되지 않습니다.

  • 가비지 수집 직전에 측정하면 가비지 수집 시작 전 가능한 한 많은 할당이 측정됩니다.

  • 가비지 수집 중 측정은 가비지 수집기 데이터 구조가 탐색에 적합한 상태가 아니고 완전한 결과를 제공하지 못할 수 있으므로 문제가 될 수 있습니다. 이는 의도된 것입니다.

  • 동시 가비지 수집을 설정하여 워크스테이션 가비지 수집을 사용할 경우 회수되는 개체가 압축되지 않아 힙 크기가 동일하거나 더 클 수 있습니다(조각화로 인해 크기가 더 크게 보일 수 있음).

  • 실제 메모리 부하가 너무 높은 경우 2세대의 동시 가비지 수집이 지연됩니다.

다음 절차에서는 관리되는 힙을 측정할 수 있도록 중단점을 설정하는 방법을 설명합니다.

가비지 수집의 끝에 중단점을 설정하려면

  • SOS 디버거 확장이 로드된 상태로 WinDbg에서 다음 명령을 입력합니다.

    bp mscorwks!WKS::GCHeap::RestartEE "j (dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2) 'kb';'g'"

    여기서 GcCondemnedGeneration은 원하는 세대로 설정됩니다. 이 명령을 사용하려면 전용 기호가 필요합니다.

    이 명령은 가비지 수집에서 2세대 개체가 회수된 후 RestartEE가 실행되면 강제로 중단됩니다.

    서버 가비지 수집에서는 하나의 스레드만 RestartEE를 호출하므로 2세대 가비지 수집 중 한 번만 중단점이 발생합니다.

맨 위로 이동

성능 검사 절차

이 단원에서는 성능 문제의 원인을 격리하기 위한 다음과 같은 절차에 대해 설명합니다.

  • 문제가 가비지 수집으로 인한 것인지 확인합니다.

  • 메모리 부족 예외가 관리되는 예외인지 확인합니다.

  • 예약할 수 있는 가상 메모리 양을 확인합니다.

  • 실제 메모리가 충분한지 확인합니다.

  • 관리되는 힙이 커밋 중인 메모리 양을 확인합니다.

  • 관리되는 힙이 예약한 메모리 양을 확인합니다.

  • 2세대에서 대형 개체를 확인합니다.

  • 개체에 대한 참조를 확인합니다.

  • 종료자가 실행되었는지 여부를 확인합니다.

  • 종료하려고 대기 중인 개체가 있는지 확인합니다.

  • 관리되는 힙에서 여유 공간의 양을 확인합니다.

  • 고정된 개체의 수를 확인합니다.

  • 가비지 수집의 시간 길이를 확인합니다.

  • 가비지 수집을 트리거한 항목을 확인합니다.

  • 높은 CPU 사용량이 가비지 수집으로 인한 것인지 확인합니다.

문제가 가비지 수집으로 인한 것인지 확인하려면

  • 다음 두 가지 메모리 성능 카운터를 검사합니다.

    • % Time in GC. 마지막 가비지 수집 주기 이후 가비지 수집을 수행하는 데 사용한 경과 시간의 비율을 나타냅니다. 이 카운터를 사용하면 가비지 수집기가 관리되는 힙 공간을 제공하기 위해 너무 많은 시간을 소비하고 있는지 여부를 확인할 수 있습니다. 가비지 수집에 소비된 시간이 상대적으로 낮으면 관리되는 힙 이외의 리소스 문제일 가능성이 있습니다. 동시 또는 백그라운드 가비지 수집과 관련된 경우 이 카운터는 정확하지 않을 수 있습니다.

    • # Total committed Bytes. 현재 가비지 수집기에 의해 커밋된 가상 메모리의 양을 나타냅니다. 이 카운터를 사용하면 가비지 수집기에서 소비된 메모리가 현재 응용 프로그램에서 사용하는 메모리에 비해 과도한지 여부를 확인할 수 있습니다.

    대부분의 메모리 성능 카운터는 각 가비지 수집이 끝날 때 업데이트됩니다. 따라서 정보가 필요한 현재의 조건은 반영되지 않을 수 있습니다.

메모리 부족 예외가 관리되는 예외인지 확인하려면

  1. WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 예외 출력(pe) 명령을 입력합니다.

    !pe

    예외가 관리되는 예외인 경우 다음 예제에서와 같이 예외 유형으로 OutOfMemoryException이 표시됩니다.

    Exception object: 39594518
    Exception type: System.OutOfMemoryException
    Message: <none>
    InnerException: <none>
    StackTrace (generated):
    
  2. 출력에 예외가 지정되지 않으면 메모리 부족 예외가 발생한 스레드를 확인해야 합니다. 디버거에 다음 명령을 입력하여 모든 스레드와 해당 호출 스택을 표시합니다.

    ~*kb

    예외 호출이 포함된 스택이 있는 스레드는 RaiseTheException 인수로 표시됩니다. 이 개체는 관리되는 예외 개체입니다.

    28adfb44 7923918f 5b61f2b4 00000000 5b61f2b4 mscorwks!RaiseTheException+0xa0 
    
  3. 다음 명령을 사용하여 중첩된 예외를 덤프할 수 있습니다.

    !pe -nested

    발견된 예외가 없으면 관리되지 않는 코드로부터 시작된 메모리 부족 예외입니다.

예약할 수 있는 가상 메모리 양을 확인하려면

  • SOS 디버거 확장이 로드된 WinDbg 디버거에서 다음 명령을 입력하여 가장 큰 여유 영역을 가져옵니다.

    !address -summary

    가장 큰 여유 영역은 다음 출력과 같이 표시됩니다.

    Largest free region: Base 54000000 - Size 0003A980
    

    이 예제에서 가장 큰 여유 영역의 크기는 약 24000KB(16진수 3A980)입니다. 이 영역은 가비지 수집기에서 세그먼트에 필요한 것보다 훨씬 작습니다.

    또는

  • vmstat 명령을 사용합니다.

    !vmstat

    가장 큰 여유 영역은 다음 출력에서와 같이 MAXIMUM 열에서 가장 큰 값입니다.

    TYPE        MINIMUM   MAXIMUM     AVERAGE   BLK COUNT   TOTAL
    ~~~~        ~~~~~~~   ~~~~~~~     ~~~~~~~   ~~~~~~~~~~  ~~~~
    Free:
    Small       8K        64K         46K       36          1,671K
    Medium      80K       864K        349K      3           1,047K
    Large       1,384K    1,278,848K  151,834K  12          1,822,015K
    Summary     8K        1,278,848K  35,779K   51          1,824,735K
    

실제 메모리가 충분한지 확인하려면

  1. Windows 작업 관리자를 시작합니다.

  2. 성능 탭에서 커밋된 값을 확인합니다. Windows 7의 경우에는 시스템 그룹에서 **커밋(KB)**을 확인합니다.

    전체제한에 가까우면 실제 메모리가 부족합니다.

관리되는 힙이 커밋 중인 메모리 양을 확인하려면

  • # Total committed bytes 메모리 성능 카운터를 사용하여 관리되는 힙이 커밋되는 바이트 수를 가져옵니다. 가비지 수집기는 항상 전체 세그먼트가 아니라 필요에 따라 세그먼트에서 청크를 커밋합니다.

    참고참고

    # Bytes in all Heaps 성능 카운터는 관리되는 힙에서 사용되는 실제 메모리 사용량을 나타내지 않으므로 사용하지 마십시오.세대 크기는 이 값에 포함되며, 실제 임계값 크기입니다. 즉, 세대에 개체가 채워졌을 때 가비지 수집을 포함하는 크기입니다.따라서 이 값은 일반적으로 0입니다.

관리되는 힙이 예약한 메모리 양을 확인하려면

  • # Total reserved bytes 메모리 성능 카운터를 사용합니다.

    가비지 수집기에서 세그먼트에 메모리가 예약되고 eeheap 명령을 사용하여 세그먼트가 시작되는 위치를 확인할 수 있습니다.

  • WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력합니다.

    !eeheap -gc

    결과는 다음과 같습니다.

    Number of GC Heaps: 2
    ------------------------------
    Heap 0 (002db550)
    generation 0 starts at 0x02abe29c
    generation 1 starts at 0x02abdd08
    generation 2 starts at 0x02ab0038
    ephemeral segment allocation context: none
     segment    begin allocated     size
    02ab0000 02ab0038  02aceff4 0x0001efbc(126908)
    Large object heap starts at 0x0aab0038
     segment    begin allocated     size
    0aab0000 0aab0038  0aab2278 0x00002240(8768)
    Heap Size   0x211fc(135676)
    ------------------------------
    Heap 1 (002dc958)
    generation 0 starts at 0x06ab1bd8
    generation 1 starts at 0x06ab1bcc
    generation 2 starts at 0x06ab0038
    ephemeral segment allocation context: none
     segment    begin allocated     size
    06ab0000 06ab0038  06ab3be4 0x00003bac(15276)
    Large object heap starts at 0x0cab0038
     segment    begin allocated     size
    0cab0000 0cab0038  0cab0048 0x00000010(16)
    Heap Size    0x3bbc(15292)
    ------------------------------
    GC Heap Size   0x24db8(150968)
    

    "세그먼트"로 표시된 주소는 세그먼트의 시작 주소입니다.

2세대의 대형 개체를 확인하려면

  • WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력합니다.

    !dumpheap –stat

    관리되는 힙이 큰 경우 dumpheap을 완료하는 데 시간이 좀 걸릴 수 있습니다.

    출력의 마지막 몇 줄에 대부분의 공간이 사용되는 개체가 나열되므로 여기에서부터 분석을 시작할 수 있습니다. 예를 들면 다음과 같습니다.

    2c6108d4   173712     14591808 DevExpress.XtraGrid.Views.Grid.ViewInfo.GridCellInfo
    00155f80      533     15216804      Free
    7a747c78   791070     15821400 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700930     19626040 System.Collections.Specialized.ListDictionary
    2c64e36c    78644     20762016 DevExpress.XtraEditors.ViewInfo.TextEditViewInfo
    79124228   121143     29064120 System.Object[]
    035f0ee4    81626     35588936 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    40182     90664128 System.Collections.Hashtable+bucket[]
    790fa3e0  3154024    137881448 System.String
    Total 8454945 objects
    

    나열된 마지막 개체는 문자열이고 대부분의 공간을 차지합니다. 응용 프로그램을 검사하여 문자열 개체를 최적화는 방법을 확인할 수 있습니다. 150바이트에서 200바이트 사이의 문자열을 보려면 다음을 입력합니다.

    !dumpheap -type System.String -min 150 -max 200

    결과 예는 다음과 같습니다.

    Address  MT           Size  Gen
    1875d2c0 790fa3e0      152    2 System.String HighlightNullStyle_Blotter_PendingOrder-11_Blotter_PendingOrder-11
    …
    

    ID 문자열 대신 정수를 사용하는 것이 더 효율적일 수 있습니다. 동일 문자열이 수천 번 반복되는 경우 문자열 인터닝(interning)을 고려하십시오. 문자열 인터닝에 대한 자세한 내용은 String.Intern 메서드의 참조 항목을 참조하십시오.

개체에 대한 참조를 확인하려면

  • SOS 디버거 확장이 로드된 WinDbg 디버거에서 다음 명령을 입력하여 개체에 대한 참조를 나열합니다.

    !gcroot

    -or-

  • 특정 개체에 대한 참조를 확인하려면 다음 주소를 포함합니다.

    !gcroot 1c37b2ac

    스택에서 발견된 루트는 거짓 긍정일 수 있습니다. 자세한 내용을 보려면 !help gcroot 명령을 사용하십시오.

    ebx:Root:19011c5c(System.Windows.Forms.Application+ThreadContext)->
    19010b78(DemoApp.FormDemoApp)->
    19011158(System.Windows.Forms.PropertyStore)->
    … [omitted]
    1c3745ec(System.Data.DataTable)->
    1c3747a8(System.Data.DataColumnCollection)->
    1c3747f8(System.Collections.Hashtable)->
    1c376590(System.Collections.Hashtable+bucket[])->
    1c376c98(System.Data.DataColumn)->
    1c37b270(System.Data.Common.DoubleStorage)->
    1c37b2ac(System.Double[])
    Scan Thread 0 OSTHread 99c
    Scan Thread 6 OSTHread 484
    

    gcroot 명령은 완료하는데 시간이 오래 걸릴 수 있습니다. 가비지 수집으로 회수되지 않는 개체는 라이브 개체입니다. 즉, 일부 루트에 개체가 직접 또는 간접적으로 보존되어 있으므로 gcroot는 개체에 대한 경로 정보를 반환합니다. 반환된 그래프를 검사하고 이러한 개체가 계속 참조되는 이유를 확인해야 합니다.

종료자가 실행되었는지 여부를 확인하려면

  • 다음 코드가 포함된 테스트 프로그램을 실행합니다.

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

    테스트로 문제가 해결될 경우, 이러한 개체의 종료자가 일시 중단되었기 때문에 가비지 수집기가 개체를 회수하지 않은 것입니다. GC.WaitForPendingFinalizers 메서드를 사용하면 종료자가 해당 작업을 완료할 수 있어서 문제가 해결됩니다.

종료하려고 대기 중인 개체가 있는지 확인하려면

  1. WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력합니다.

    !finalizequeue

    종료할 준비가 된 개체의 수를 조사합니다. 숫자가 높으면 이러한 종료자가 처리를 전혀 수행할 수 없거나 충분히 빠른 속도로 처리하지 못하는 이유를 확인해야 합니다.

  2. 스레드의 출력을 가져오려면 다음 명령을 입력합니다.

    threads -special

    이 명령은 다음과 같은 출력을 제공합니다.

           OSID     Special thread type
        2    cd0    DbgHelper 
        3    c18    Finalizer 
        4    df0    GC SuspendEE 
    

    종료자 스레드는 현재 실행 중인 종료자(있는 경우)를 나타냅니다. 종료자 스레드가 종료자를 실행하지 않을 때는 작업 지시 이벤트를 기다리는 중입니다. 종료자 스레드는 THREAD_HIGHEST_PRIORITY로 실행되고 실행할 종료자가 있더라도 매우 빠르게 종료자 실행을 마치므로 대부분의 경우 종료자 스레드가 이 상태로 발견됩니다.

관리되는 힙에서 여유 공간의 양을 확인하려면

  • WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력합니다.

    !dumpheap -type Free -stat

    이 명령은 다음 예제에서와 같이 관리되는 힙의 모든 여유 개체의 총 크기를 표시합니다.

    total 230 objects
    Statistics:
          MT    Count    TotalSize Class Name
    00152b18      230     40958584      Free
    Total 230 objects
    
  • 0세대의 여유 공간을 확인하려면 세대별 메모리 소비 정보를 확인하기 위해 다음 명령을 입력합니다.

    !eeheap -gc

    이 명령은 다음과 비슷한 출력을 표시합니다. 마지막 줄은 임시 세그먼트를 나타냅니다.

    Heap 0 (0015ad08)
    generation 0 starts at 0x49521f8c
    generation 1 starts at 0x494d7f64
    generation 2 starts at 0x007f0038
    ephemeral segment allocation context: none
    segment  begin     allocated  size
    00178250 7a80d84c  7a82f1cc   0x00021980(137600)
    00161918 78c50e40  78c7056c   0x0001f72c(128812)
    007f0000 007f0038  047eed28   0x03ffecf0(67103984)
    3a120000 3a120038  3a3e84f8   0x002c84c0(2917568)
    46120000 46120038  49e05d04   0x03ce5ccc(63855820)
    
  • 0세대에서 사용된 공간을 계산합니다.

    ? 49e05d04-0x49521f8c

    결과는 다음과 같습니다. 0세대는 약 9MB입니다.

    Evaluate expression: 9321848 = 008e3d78
    
  • 다음 명령은 0세대 범위 내의 여유 공간을 덤프합니다.

    !dumpheap -type Free -stat 0x49521f8c 49e05d04

    결과는 다음과 같습니다.

    ------------------------------
    Heap 0
    total 409 objects
    ------------------------------
    Heap 1
    total 0 objects
    ------------------------------
    Heap 2
    total 0 objects
    ------------------------------
    Heap 3
    total 0 objects
    ------------------------------
    total 409 objects
    Statistics:
          MT    Count TotalSize Class Name
    0015a498      409   7296540      Free
    Total 409 objects
    

    이 출력에는 힙에서 0세대 부분이 개체에 대해 9MB 공간을 사용 중이고 여유 공간이 7MB인 것을 보여 줍니다. 이 분석은 0세대가 조각화에 기여하는 정도를 보여 줍니다. 이 힙 사용량은 장기 개체에 의한 조각화의 원인으로 전체 수량에서 제외됩니다.

고정된 개체의 수를 확인하려면

  • WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력합니다.

    !gchandles

    표시된 통계에는 다음 예제에서 보여 주는 것처럼 고정된 핸들의 수가 포함됩니다.

    GC Handle Statistics:
    Strong Handles:      29
    Pinned Handles:      10
    

가비지 수집의 시간 길이를 확인하려면

  • % Time in GC 메모리 성능 카운터를 검사합니다.

    이 값은 샘플 간격 시간을 사용하여 계산됩니다. 카운터는 각 가비지 수집의 끝에 업데이트되므로 현재 샘플은 해당 기간 동안 수집이 수행되지 않은 경우 이전 샘플과 값이 동일합니다.

    수집 시간은 샘플 간격 시간과 백분율 값을 곱하여 계산됩니다.

    다음 데이터는 8초로 구성된 연구에서 2초의 샘플링 간격 4개를 보여 줍니다. Gen0, Gen1 및 Gen2 열에는 해당 세대에 대해 이 기간 동안 수행된 가비지 수집 횟수를 보여 줍니다.

    Interval    Gen0    Gen1    Gen2    % Time in GC
           1       9       3       1              10
           2      10       3       1               1
           3      11       3       1               3
           4      11       3       1               3
    

    이 정보에는 가비지 수집이 발생한 시간이 표시되지 않지만 특정 간격 동안 발생한 가비지 수집 횟수를 확인할 수 있습니다. 최악의 경우를 가정할 경우 10번째 0세대 가비지 수집은 두 번째 기간의 시작 시간에 완료되고, 11번째 0세대 가비지 수집은 5번째 간격의 종료 시간에 완료됩니다. 10번째 가비지 수집의 끝과 11번째 가비지 수집의 끝 사이의 시간은 약 2초이며, 성능 카운터는 3%로 표시되므로 11번째 0세대 가비지 수집의 기간은 2초 * 3% = 60ms입니다.

    이 예제에는 5개 기간이 있습니다.

    Interval    Gen0    Gen1    Gen2     % Time in GC
           1       9       3       1                3
           2      10       3       1                1
           3      11       4       2                1
           4      11       4       2                1
           5      11       4       2               20
    

    두 번째 2세대 가비지 수집은 세 번째 간격 중에 시작되고 5번째 간격에 종료됩니다. 최악의 경우를 가정할 경우 마지막 가비지 수집은 두 번째 간격의 시작 시간에 종료된 0세대 수집이고, 2세대 가비지 수집은 5번째 간격의 종료 시간에 완료됩니다. 따라서 0세대 가비지 수집의 끝과 2세대 가비지 수집의 끝 사이의 시간은 4초입니다. % Time in GC 카운터는 20%이므로, 2세대 가비지 수집에 소비될 수 있는 최대 시간은 4초 * 20% = 800ms입니다.

  • 또는 가비지 수집 ETW 이벤트를 사용하여 가비지 수집의 길이를 확인하고, 정보를 분석하여 가비지 수집의 기간을 확인할 수 있습니다.

    예를 들어 다음 데이터에서는 비동시 가비지 수집 중 발생한 이벤트 시퀀스를 보여 줍니다.

    Timestamp    Event name
    513052        GCSuspendEEBegin_V1
    513078        GCSuspendEEEnd
    513090        GCStart_V1
    517890        GCEnd_V1
    517894        GCHeapStats
    517897        GCRestartEEBegin
    517918        GCRestartEEEnd
    

    관리되는 스레드의 일시 중단은 26us(GCSuspendEEEnd – GCSuspendEEBegin_V1)가 걸렸습니다.

    실제 가비지 수집은 4.8ms(GCEnd_V1 – GCStart_V1)가 걸렸습니다.

    관리되는 스레드 다시 시작은 21us(GCRestartEEEnd – GCRestartEEBegin)가 걸렸습니다.

    다음 출력에서는 백그라운드 가비지 수집에 대한 예를 제공하고 프로세스, 스레드 및 이벤트 필드를 포함합니다. 모든 데이터가 표시된 것은 아닙니다.

    timestamp(us)    event name            process    thread    event field
    42504385        GCSuspendEEBegin_V1    Test.exe    4372             1
    42504648        GCSuspendEEEnd         Test.exe    4372        
    42504816        GCStart_V1             Test.exe    4372        102019
    42504907        GCStart_V1             Test.exe    4372        102020
    42514170        GCEnd_V1               Test.exe    4372        
    42514204        GCHeapStats            Test.exe    4372        102020
    42832052        GCRestartEEBegin       Test.exe    4372        
    42832136        GCRestartEEEnd         Test.exe    4372        
    63685394        GCSuspendEEBegin_V1    Test.exe    4744             6
    63686347        GCSuspendEEEnd         Test.exe    4744        
    63784294        GCRestartEEBegin       Test.exe    4744        
    63784407        GCRestartEEEnd         Test.exe    4744        
    89931423        GCEnd_V1               Test.exe    4372        102019
    89931464        GCHeapStats            Test.exe    4372        
    

    42504816의 GCStart_V1 이벤트는 마지막 필드가 1이므로 백그라운드 가비지 수집임을 나타냅니다. 이 값은 가비지 수집 번호가 됩니다. 102019.

    백그라운드 가비지 수집을 시작하기 전에 임시 가비지 수집에 필요하므로 GCStart 이벤트가 발생합니다. 이 값은 가비지 수집 번호가 됩니다. 102020.

    42514170에서 가비지 수집 번호입니다. 102020이 완료됩니다. 관리되는 스레드는 이 지점에서 다시 시작됩니다. 이 작업은 이 백그라운드 가비지 수집을 트리거한 스레드 4372에서 완료되었습니다.

    스레드 4744에서는 일시 중단이 발생합니다. 이 시간에서만 백그라운드 가비지 수집이 관리되는 스레드를 일시 중단해야 합니다. 이 기간은 약 99ms((63784407-63685394)/1000)입니다.

    백그라운드 가비지 수집에 대한 GCEnd 이벤트는 89931423에 있습니다. 즉, 백그라운드 가비지 수집이 약 47초 동안 유지됩니다((89931423-42504816)/1000).

    관리되는 스레드가 실행되는 동안 임시 가비지 수집이 발생하는 횟수를 볼 수 있습니다.

가비지 수집을 트리거한 항목을 확인하려면

  • WinDbg 또는 SOS 디버거 확장이 로드된 Visual Studio 디버거에서 다음 명령을 입력하여 해당 호출 스택과 함께 모든 스레드를 표시합니다.

    ~*kb

    이 명령은 다음과 비슷한 출력을 표시합니다.

    0012f3b0 79ff0bf8 mscorwks!WKS::GCHeap::GarbageCollect 
    0012f454 30002894 mscorwks!GCInterface::CollectGeneration+0xa4
    0012f490 79fa22bd fragment_ni!request.Main(System.String[])+0x48
    

    가비지 수집이 운영 체제의 메모리 부족 알림으로 인해 발생한 경우, 호출 스택이 비슷하지만 스레드가 종료자 스레드입니다. 종료자 스레드는 비동기 메모리 부족 알림을 가져오고 가비지 수집을 수행합니다.

    가비지 수집이 메모리 할당으로 인해 발생한 경우 스택이 다음과 같이 표시됩니다.

    0012f230 7a07c551 mscorwks!WKS::GCHeap::GarbageCollectGeneration
    0012f2b8 7a07cba8 mscorwks!WKS::gc_heap::try_allocate_more_space+0x1a1
    0012f2d4 7a07cefb mscorwks!WKS::gc_heap::allocate_more_space+0x18
    0012f2f4 7a02a51b mscorwks!WKS::GCHeap::Alloc+0x4b
    0012f310 7a02ae4c mscorwks!Alloc+0x60
    0012f364 7a030e46 mscorwks!FastAllocatePrimitiveArray+0xbd
    0012f424 300027f4 mscorwks!JIT_NewArr1+0x148
    000af70f 3000299f fragment_ni!request..ctor(Int32, Single)+0x20c
    0000002a 79fa22bd fragment_ni!request.Main(System.String[])+0x153
    

    JIT(just-in-time) 도우미(JIT_New*)는 결국 GCHeap::GarbageCollectGeneration을 호출합니다. 2세대 가비지 수집이 할당으로 인해 발생한 것으로 파악될 경우 2세대 가비지 수집으로 수집된 개체를 확인하고 이를 방지하기 위한 방법을 결정해야 합니다. 즉, 2세대 가비지 수집의 시작과 끝 사이의 차이를 파악하고 2세대 수집을 일으킨 개체를 확인할 수 있습니다.

    예를 들어 디버거에서 다음 명령을 입력하여 2세대 수집의 시작을 표시합니다.

    !dumpheap –stat

    예제 출력(대부분의 공간을 사용하는 개체의 축약 표시):

    79124228    31857      9862328 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    00155f80    21248     12256296      Free
    79103b6c   297003     13068132 System.Threading.ReaderWriterLock
    7a747ad4   708732     14174640 System.Collections.Specialized.HybridDictionary
    7a747c78   786498     15729960 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    035f0ee4    89192     38887712 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    7912c444    91616     71887080 System.Double[]
    791242ec    32451     82462728 System.Collections.Hashtable+bucket[]
    790fa3e0  2459154    112128436 System.String
    Total 6471774 objects
    

    2세대 끝에서 명령을 반복합니다.

    !dumpheap –stat

    예제 출력(대부분의 공간을 사용하는 개체의 축약 표시):

    79124228    26648      9314256 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    79103b6c   296770     13057880 System.Threading.ReaderWriterLock
    7a747ad4   708730     14174600 System.Collections.Specialized.HybridDictionary
    7a747c78   786497     15729940 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    00155f80    13806     34007212      Free
    035f0ee4    89187     38885532 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    32370     82359768 System.Collections.Hashtable+bucket[]
    790fa3e0  2440020    111341808 System.String
    Total 6417525 objects
    

    double[] 개체는 출력 끝에서 사라집니다. 즉, 개체가 수집되었습니다. 이러한 개체는 약 70MB를 차지합니다. 남은 개체는 크게 변경되지 않습니다. 따라서 이러한 double[] 개체는 2세대 가비지 수집이 발생한 이유입니다. 다음 단계에서는 double[] 개체가 있는 이유와 왜 사라졌는지를 확인해야 합니다. 코드 개발자에게 이러한 개체가 시작된 위치를 확인하거나 gcroot 명령을 사용할 수 있습니다.

높은 CPU 사용량이 가비지 수집으로 인한 것인지 확인하려면

  • % Time in GC 메모리 성능 카운터 값을 처리 시간과 연결합니다.

    % Time in GC 값이 처리 시간과 동시에 크게 상승하면 가비지 수집으로 인해 CPU 사용량이 높아진 것입니다. 그렇지 않으면 사용량이 높은 지점을 찾기 위해 응용 프로그램을 프로파일링합니다.

참고 항목

개념

가비지 수집