Microsoft MVP성태의 닷넷 이야기
.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미 [링크 복사], [링크+제목 복사]
조회: 15360
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13600정성태4/18/2024271닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024287닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024316닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024383닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024758닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024879닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241008닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241050닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241206C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241167닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241072Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241142닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241196닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241154오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241296Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241094Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241047개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241157Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241417Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241586개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241136닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241493오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241628닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241874닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241543닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...