Microsoft MVP성태의 닷넷 이야기
.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미 [링크 복사], [링크+제목 복사],
조회: 15599
글쓴 사람
정성태 (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)
12970정성태2/15/20228209.NET Framework: 1156. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 h264 형식의 파일로 쓰기 [1]파일 다운로드1
12969정성태2/14/20226689개발 환경 구성: 638. Visual Studio의 Connection Manager 기능(Remote SSH 관리)을 위한 명령행 도구 - 두 번째 이야기파일 다운로드1
12968정성태2/14/20226917오류 유형: 794. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for '...'.
12967정성태2/14/20227266VC++: 153. Visual C++ - C99 표준의 Compund Literals 빌드 방법 [4]
12966정성태2/13/20227090.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
12965정성태2/13/20227107.NET Framework: 1154. "Hanja Hangul Project v1.01 (파이썬)"의 C# 버전
12964정성태2/11/20227434.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/20228160.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/20227996오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20228136.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/20227541개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20228261.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20228292.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20227879개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/20227125.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/20226591개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/20226416VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20226626Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/20227131.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/20227105.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20226668.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226529.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226645.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227822개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/20226321오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226613오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...