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

(시리즈 글이 12개 있습니다.)
Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미
; https://www.sysnet.pe.kr/2/0/1850

.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미
; https://www.sysnet.pe.kr/2/0/1852

.NET Framework: 626. Commit 메모리가 낮은 상황에서도 메모리 부족(Out-of-memory) 예외 발생
; https://www.sysnet.pe.kr/2/0/11110

VC++: 107. VirtualAlloc, HeapAlloc, GlobalAlloc, LocalAlloc, malloc, new의 차이점
; https://www.sysnet.pe.kr/2/0/11152

Windows: 136. Memory-mapped File은 Private Bytes 크기에 포함될까요?
; https://www.sysnet.pe.kr/2/0/11159

.NET Framework: 845. C# - 윈도우 작업 관리자와 리소스 모니터의 메모리 값을 구하는 방법
; https://www.sysnet.pe.kr/2/0/11950

Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법
; https://www.sysnet.pe.kr/2/0/13133

.NET Framework: 2073. C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력
; https://www.sysnet.pe.kr/2/0/13174

.NET Framework: 2074. C# - 스택 메모리에 대한 여유 공간 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13180

Linux: 57. C# - 리눅스 프로세스 메모리 정보
; https://www.sysnet.pe.kr/2/0/13221

닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI)
; https://www.sysnet.pe.kr/2/0/13889

닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)
; https://www.sysnet.pe.kr/2/0/13890




.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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  [145]  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1462정성태7/5/201328111VC++: 69. geek스러운 C/C++ 퀴즈 문제 [2]
1461정성태6/27/201344723.NET Framework: 373. C# 문자열의 인코딩이란?
1460정성태6/17/201326529.NET Framework: 372. PerformanceCounter - Category does not exist. [1]
1459정성태6/15/201330140Windows: 74. 한글 키가 아닌 영문 키를 기본으로 선택하는 방법 [5]
1458정성태6/13/201331002.NET Framework: 371. CAS Lock 방식이 과연 성능에 얼마나 도움이 될까요? [1]파일 다운로드1
1457정성태6/13/201326967개발 환경 구성: 192. "Probabilistic Programming and Bayesian Methods for Hackers" 예제 코드 실행 방법
1456정성태6/5/201335633.NET Framework: 370. C# - WebKit .NET 사용 [2]파일 다운로드1
1455정성태6/1/201329564.NET Framework: 369. ThreadPool.QueueUserWorkItem의 실행 지연 [4]파일 다운로드1
1454정성태5/31/201327507Java: 15. Java 7 Control Panel 실행시키는 방법
1453정성태5/22/201326641기타: 32. Microsoft FTP 사이트에 접속하는 방법
1452정성태5/21/201334201Windows: 73. TabProcGrowth 값 삭제 후 IE를 실행시키면 다시 복원되는 경우 [3]
1451정성태5/17/201333099Windows: 72. 윈도우 서버 2012 기초 사용법
1450정성태5/16/201323953오류 유형: 176. SQL10007N Message "0" could not be retrieved. Reason code: "3"
1449정성태5/15/201331018오류 유형: 175. SpeechRecognitionEngine 사용 시 오류 유형 2가지
1448정성태5/14/201326058VC++: 68. #pragma warning(disable: ...)로 오류 제어가 안된다면?
1447정성태5/3/201328114개발 환경 구성: 191. Debugging Tools for Windows 독립 설치 버전 [1]
1446정성태4/30/201328753.NET Framework: 368. Encoding 타입의 대체(fallback) 메카니즘 [1]
1445정성태4/26/201326841디버깅 기술: 54. NT 서비스의 Main 메서드 안에서 Process.GetProcessesByName 호출 시 멈춤 현상 [1]
1444정성태4/26/201330828기타: 31. Internet Explorer: 자바스크립트로 숨겨진 파일 다운로드 경로를 알아내는 방법 [1]
1443정성태4/24/201326103개발 환경 구성: 190. Azure PaaS 웹 응용 프로그램 배포 후 SMTP 서버 구성 [2]
1442정성태4/21/201330197기타: 30. 마이크로소프트 워드의 CPU 점유 현상으로 글자 입력이 느려졌다면? [1]
1441정성태4/21/201336683.NET Framework: 367. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 [14]
1440정성태4/19/201325292오류 유형: 174. dumpbin.exe 실행시 mspdb110.dll 로드 오류
1439정성태4/18/201329194VS.NET IDE: 76. Visual Studio 2012와 Itanium 빌드 옵션 [2]
1438정성태4/17/201328859.NET Framework: 366. 다른 프로세스에 환경 변수 설정하는 방법 - 두 번째 이야기 [1]파일 다운로드1
1437정성태4/17/201329029VC++: 67. CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거
... 136  137  138  139  140  141  142  143  144  [145]  146  147  148  149  150  ...