Microsoft MVP성태의 닷넷 이야기
.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미 [링크 복사], [링크+제목 복사],
조회: 15608
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

.NET CLR Memory 성능 카운터의 의미

다음의 글에 나오는 성능 카운터를 한번 볼까요? ^^

Memory Performance Counters
; https://learn.microsoft.com/ko-kr/previous-versions/x2tyfybc(v=vs.110)

그중에서 먼저 아래의 성능카운터를 살펴보겠습니다.

# Total committed Bytes: Displays the amount of virtual memory, in bytes, currently committed by the garbage collector. Committed memory is the physical memory for which space has been reserved in the disk paging file.

# Total reserved Bytes: Displays the amount of virtual memory, in bytes, currently reserved by the garbage collector. Reserved memory is the virtual memory space reserved for the application when no disk or main memory pages have been used.


지난 글을 보셨다면,

작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미
; https://www.sysnet.pe.kr/2/0/1850

이제 "# Total committed Bytes"와 "# Total reserved Bytes"의 차이점을 아실 수 있을 것입니다. 전자는 실제로 메모리/디스크에 할당이 된 크기이고, 후자는 주소 공간 예약까지를 포함한 크기입니다. 따라서, 이름에서 알 수 있듯이 ".NET CLR Memory" 범주의 "# Total committed Bytes", "# Total reserved Bytes" 성능 카운터가 가리키는 수치는 '가상 메모리' 중에서도 "CLR"에 의해 할당된 크기만을 나타냅니다. 즉, GC Heap으로 인한 가상 메모리 예약/할당 크기입니다.

코드에서 다음과 같이 수치를 구하면,

Process process = System.Diagnostics.Process.GetCurrentProcess();
Console.WriteLine((process.VirtualMemorySize64 / 1024).ToString("#,#"));

VirtualMemorySize64 숫자는 언제나 ".NET CLR Memory" 범주의 "# Total reserved Bytes" 수치보다 큽니다. (참고로, 성능 카운터에 VirtualMemorySize64와 동일한 크기를 나타내는 수치로 Process 범주의 "Virtual Bytes"가 해당합니다.)




그다음 GC Heap 메모리 쪽의 수치를 볼까요?

# Bytes in all Heaps: Gen 1 Heap Size, Gen 2 Heap Size, and Large Object Heap Size counters This counter indicates the current memory allocated in bytes on the garbage collection heaps.

Gen 0 heap size: Displays the maximum bytes that can be allocated in generation 0; it does not indicate the current number of bytes allocated in generation 0.

Gen 1 heap size: Displays the current number of bytes in generation 1; this counter does not display the maximum size of generation 1. Objects are not directly allocated in this generation; they are promoted from previous generation 0 garbage collections. This counter is updated at the end of a garbage collection, not at each allocation.

Gen 2 heap size: Displays the current number of bytes in generation 2. Objects are not directly allocated in this generation; they are promoted from generation 1 during previous generation 1 garbage collections. This counter is updated at the end of a garbage collection, not at each allocation.

Large Object Heap size: Displays the current size, in bytes, of the Large Object Heap. Objects that are greater than approximately 85,000 bytes are treated as large objects by the garbage collector and are directly allocated in a special heap; they are not promoted through the generations. This counter is updated at the end of a garbage collection, not at each allocation.


순간적인 값을 재었을 때,

# Bytes in all Heaps:           61,200,000

-------------------------------------------
Gen 0 heap size:		 3,145,728
-------------------------------------------
Gen 1 heap size:		 2,328,000
Gen 2 heap size:		24,272,000
Large Object Heap size:         34,600,000
          (Gen1 + Gen2 + LOH == 61,200,000)

문서의 내용에 나온 것처럼, "# Bytes in all Heaps" 수치는 정확히 (Gen0을 제외한) "Gen1 + Gen2 + LOH Heap"의 크기와 일치합니다. 아마도 Gen 0 힙에서는 빈번하게 메모리 할당이 이뤄질 것이기 때문에 그 순간의 수치가 크게 의미가 없다고 판단한 것이 아닌가 생각됩니다. 그 외의 수치(Gen1, Gen2, LOH)는 GC가 발생하지 않으면 고정이기 때문에 의미가 있고.

그런데 "Gen 0 heap size"의 숫자가 왠지 낯설지 않습니다. 왜냐하면 3 * 1024 * 1024로 얻을 수 있는 수치로 정확히 3MB에 해당하기 때문입니다. 즉, Gen 0 힙 크기는 항상 3MB를 유지하면서 간다고 짐작할 수 있습니다.

(참고로, 여기서 밝히는 모든 수치는 상황에 따라 달라질 수 있습니다. 가령, 메모리 할당 상황에 따라 Gen 0 힙 크기가 3MB가 아닐 수도 있고, 닷넷 버전에 따라 달라질 수 있습니다.)




이와 같은 성능 카운터에 해당하는 수치를 코드로 구하는 것이 가능할까요? 물론입니다. ^^ 이를 위해서는 약간 복잡한 Inteop 코드를 포함해야 하는데요. 다음과 같이 고정적인 인터페이스 및 구조체 정의가 필요합니다.

[Flags]
public enum COR_GC_STAT_TYPES
{
    COR_GC_COUNTS = 0x1,
    COR_GC_MEMORYUSAGE = 0x2
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct _COR_GC_STATS
{
    public uint Flags;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint ExplicitGCCount;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public uint[] GenCollectionsTaken;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint CommittedKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint ReservedKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint Gen0HeapSizeKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint Gen1HeapSizeKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint Gen2HeapSizeKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint LargeObjectHeapSizeKBytes;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint KBytesPromotedFromGen0;
    [ComAliasName("mscoree.ULONG_PTR")]
    public uint KBytesPromotedFromGen1;
}

[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct _COR_GC_THREAD_STATS
{
    public ulong PerThreadAllocation;
    public uint Flags;
}

[ComImport, InterfaceType((short)1), Guid("FAC34F6E-0DCD-47B5-8021-531BC5ECCA63")]
public interface IGCHost
{
    void SetGCStartupLimits([In] uint SegmentSize, [In] uint MaxGen0Size);
    void Collect([In] int Generation);
    void GetStats([In, Out] ref _COR_GC_STATS pStats);
    void GetThreadStats([In] ref uint pFiberCookie, [In, Out] ref _COR_GC_THREAD_STATS pStats);
    void SetVirtualMemLimit([In, ComAliasName("mscoree.ULONG_PTR")] uint sztMaxVirtualMemMB);
}

[ComImport, InterfaceType((short)1), Guid("F31D1788-C397-4725-87A5-6AF3472C2791")]
public interface IGCThreadControl
{
    void ThreadIsBlockingForSuspension();
    void SuspensionStarting();
    void SuspensionEnding(uint Generation);
}

[ComImport, Guid("5513D564-8374-4CB9-AED9-0083F4160A1D"), InterfaceType((short)1)]
public interface IGCHostControl
{
    void RequestVirtualMemLimit([In] uint sztMaxVirtualMemMB, [In, Out] ref uint psztNewMaxVirtualMemMB);
}

[ComImport, InterfaceType((short)1), Guid("23D86786-0BB5-4774-8FB5-E3522ADD6246")]
public interface IDebuggerThreadControl
{
    void ThreadIsBlockingForDebugger();
    void ReleaseAllRuntimeThreads();
    void StartBlockingForDebugger(uint dwUnused);
}

[ComImport, Guid("5C2B07A5-1E98-11D3-872F-00C04F79ED0D"), InterfaceType((short)1)]
public interface ICorConfiguration
{
    void SetGCThreadControl([In, MarshalAs(UnmanagedType.Interface)] IGCThreadControl pGCThreadControl);
    void SetGCHostControl([In, MarshalAs(UnmanagedType.Interface)] IGCHostControl pGCHostControl);
    void SetDebuggerThreadControl([In, MarshalAs(UnmanagedType.Interface)] IDebuggerThreadControl pDebuggerThreadControl);
    void AddDebuggerSpecialThread([In] uint dwSpecialThreadId);
}

[ComImport, ComConversionLoss, Guid("CB2F6722-AB3A-11D2-9C40-00C04FA30A3E"), InterfaceType((short)1)]
public interface ICorRuntimeHost
{
    void CreateLogicalThreadState();
    void DeleteLogicalThreadState();
    void SwitchInLogicalThreadState([In] ref uint pFiberCookie);
    void SwitchOutLogicalThreadState([Out] IntPtr pFiberCookie);
    void LocksHeldByLogicalThread(out uint pCount);
    void MapFile([In] IntPtr hFile, out IntPtr hMapAddress);
    void GetConfiguration([MarshalAs(UnmanagedType.Interface)] out ICorConfiguration pConfiguration);
    void Start();
    void Stop();
    void CreateDomain([In, MarshalAs(UnmanagedType.LPWStr)] string pwzFriendlyName, [In, MarshalAs(UnmanagedType.IUnknown)] object pIdentityArray, [MarshalAs(UnmanagedType.IUnknown)] out object pAppDomain);
    void GetDefaultDomain([MarshalAs(UnmanagedType.IUnknown)] out object pAppDomain);
    void EnumDomains(out IntPtr hEnum);
    void NextDomain([In] IntPtr hEnum, [MarshalAs(UnmanagedType.IUnknown)] out object pAppDomain);
    void CloseEnum([In] IntPtr hEnum);
    void CreateDomainEx([In, MarshalAs(UnmanagedType.LPWStr)] string pwzFriendlyName, [In, MarshalAs(UnmanagedType.IUnknown)] object pSetup, [In, MarshalAs(UnmanagedType.IUnknown)] object pEvidence, [MarshalAs(UnmanagedType.IUnknown)] out object pAppDomain);
    void CreateDomainSetup([MarshalAs(UnmanagedType.IUnknown)] out object pAppDomainSetup);
    void CreateEvidence([MarshalAs(UnmanagedType.IUnknown)] out object pEvidence);
    void UnloadDomain([In, MarshalAs(UnmanagedType.IUnknown)] object pAppDomain);
    void CurrentDomain([MarshalAs(UnmanagedType.IUnknown)] out object pAppDomain);
}

그다음, ICorRuntimeHost 인터페이스를 구현한 객체를 구하고,

static void Main(string[] args)
{
    ICorRuntimeHost host = CoCreateInstance("{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}") as ICorRuntimeHost;
}

public static object CoCreateInstance(string guid)
{
    Guid coClassGuid = new Guid(guid);
    System.Type type = System.Type.GetTypeFromCLSID(coClassGuid);

    return Activator.CreateInstance(type);
} 

ICorRuntimeHost 객체로부터 IGCHost를 QI(QueryInterface)할 수 있습니다.

IGCHost gcHost32 = host as IGCHost;

이후, 단순하게 IGCHost.GetStats 메서드를 호출해 주는 것만으로 성능 카운터에서 본 해당 수치들을 가져올 수 있습니다.

public uint Flags;
public uint ExplicitGCCount;
public uint[] GenCollectionsTaken;
public uint CommittedKBytes;
public uint ReservedKBytes;
public uint Gen0HeapSizeKBytes;
public uint Gen1HeapSizeKBytes;
public uint Gen2HeapSizeKBytes;
public uint LargeObjectHeapSizeKBytes;
public uint KBytesPromotedFromGen0;
public uint KBytesPromotedFromGen1;

이렇게!

static void Main(string[] args)
{
    // ...[생략]...
    while (true)
    {
        long committedMBytes = GetGCState32(gcDetails, gcHost32);

        // ...[생략]...
    }
}

private static long GetGCState32(float[] gcDetails, IGCHost gcHost)
{
    _COR_GC_STATS stats = new _COR_GC_STATS();

    if (gcHost != null)
    {
        try
        {
            stats.Flags = (int)(COR_GC_STAT_TYPES.COR_GC_COUNTS | COR_GC_STAT_TYPES.COR_GC_MEMORYUSAGE);
            gcHost.GetStats(ref stats);

            gcDetails[0] = stats.Gen0HeapSizeKBytes;
            gcDetails[1] = stats.Gen1HeapSizeKBytes;
            gcDetails[2] = stats.Gen2HeapSizeKBytes;
            gcDetails[3] = stats.LargeObjectHeapSizeKBytes;

            return stats.CommittedKBytes;
        }
        catch
        {
        }
    }

    return 0;
}

주의할 점이 하나 있다면, IGCHost.GetStats 메서드의 호출이 응용 프로그램 초기에는 아무런 값도 반환하지 않는다는 것입니다. 그리고 32비트와 64비트에 따라 COR_GC_STATS의 구조체 내부 필드 크기가 달라진다는 것도 유의해야 하고.

(첨부한 프로젝트의 코드는 32비트/64비트에 따른 지원을 포함하고 있으니 참고하세요. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 12/16/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...
NoWriterDateCnt.TitleFile(s)
12945정성태1/28/20226616오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20229069.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227887.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/20228148.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/20229555오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/20226894오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/20227632.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
12938정성태1/24/20229991개발 환경 구성: 633. Docker Desktop + k8s 환경에서 local 이미지를 사용하는 방법
12937정성태1/24/20227824.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c) [2]파일 다운로드1
12936정성태1/22/20227767.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)파일 다운로드1
12935정성태1/22/20227996.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/20227569오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/20228084.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/20228519.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20226549개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/20227284개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/20227083개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
12928정성태1/18/20227178개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226958개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227497오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227537개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20229064.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227947개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20227033개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225933개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
12920정성태1/14/20226704오류 유형: 786. Camtasia - An error occurred with the camera: Failed to Add Video Sampler.
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...