Microsoft MVP성태의 닷넷 이야기
.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미 [링크 복사], [링크+제목 복사],
조회: 15606
글쓴 사람
정성태 (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)
13096정성태7/8/20228673.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/20226719Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
13094정성태7/6/20226475오류 유형: 816. Golang - "short write" 오류 원인
13093정성태7/5/20227347.NET Framework: 2029. C# - HttpWebRequest로 localhost 접속 시 2초 이상 지연
13092정성태7/3/20228322.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식파일 다운로드1
13091정성태7/3/20227230.NET Framework: 2027. C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
13090정성태6/29/20226301오류 유형: 815. PyPI에 업로드한 패키지가 반영이 안 되는 경우
13089정성태6/28/20226775개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
13088정성태6/27/20225753개발 환경 구성: 645. "Developer Command Prompt for VS 2022" 명령행 환경의 폰트를 바꾸는 방법
13087정성태6/23/20228801스크립트: 41. 파이썬 - FastAPI / uvicorn 호스팅 환경에서 asyncio 사용하는 방법 [1]
13086정성태6/22/20228242.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20228329.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226789개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227432.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226491.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
13081정성태6/17/20226501.NET Framework: 2022. C# - .NET 7 Preview 5 신규 기능 - System.IO.Stream ReadExactly / ReadAtLeast파일 다운로드1
13080정성태6/17/20227168개발 환경 구성: 643. Visual Studio 2022 17.2 버전에서 C# 11 또는 .NET 7.0 preview 적용
13079정성태6/17/20224780오류 유형: 814. 파이썬 - Error: The file/path provided (...) does not appear to exist
13078정성태6/16/20226976.NET Framework: 2021. WPF - UI Thread와 Render Thread파일 다운로드1
13077정성태6/15/20227202스크립트: 40. 파이썬 - PostgreSQL 환경 구성
13075정성태6/15/20226160Linux: 50. Linux - apt와 apt-get의 차이 [2]
13074정성태6/13/20226482.NET Framework: 2020. C# - NTFS 파일에 사용자 정의 속성값 추가하는 방법파일 다운로드1
13073정성태6/12/20226764Windows: 207. Windows Server 2022에 도입된 WSL 2
13072정성태6/10/20227038Linux: 49. Linux - ls 명령어로 출력되는 디렉터리 색상 변경 방법
13071정성태6/9/20227639스크립트: 39. Python에서 cx_Oracle 환경 구성
13070정성태6/8/20227512오류 유형: 813. Windows 11에서 입력 포커스가 바뀌는 문제 [1]
... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...