Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 7개 있습니다.)
.NET 64비트 응용 프로그램에서 왜 (2GB) OutOfMemoryException 예외가 발생할까?

(2018-08-18 업데이트) .NET 4.5부터 2GB 한계를 넘는 옵션이 추가됐습니다.
.NET 4.5의 2GB 힙 한계 극복
; https://www.sysnet.pe.kr/2/0/1403




재현 가능한 예제 코드는 다음과 같습니다.

static void Main(string[] args)
{
    HashSet<long> t = new HashSet<long>();

    for (long i = 0; i < Int32.MaxValue; i++)
    {
        t.Add(i);
    }
}

x64 또는 AnyCPU로 빌드하고 64비트 운영체제가 설치된 PC에서 실행하게 되면 다음과 같이 메모리를 소비하다가 이내 OutOfMemoryException 예외에 걸려 버립니다.

oom_dotnet_x64_at_2gbheap_1.png

이에 대한 원인을 다음의 글에서 아주 상세하게 설명해 주고 있습니다.

 BigArray<T>, getting around the 2GB array size limit 
; http://blogs.msdn.com/b/joshwil/archive/2005/08/10/450202.aspx

즉, CLR 객체 하나가 가질 수 있는 최대 메모리 용량이 2GB로 제한되어 있기 때문입니다. 재미 삼아서 확인해 볼까요? ^^

OutOfMemoryException 예외가 발생했을 때, windbg를 연결하고 다음과 같이 명령을 내리면,

0:004> .loadby sos clr

0:004> !dumpheap -stat
total 0 objects
Statistics:
              MT    Count    TotalSize Class Name
000007feea3d3ef8        1           24 System.Collections.Generic.GenericEqualityComparer`1[[System.Int64, mscorlib]]
...[생략]...
000007fee99ec7e8       15    287976920 System.Int32[]
000007ff00056ce0        2   1727850288 System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
Total 646 objects

용량이, 약 1.7GB 정도 되는군요. 좀 더 자세하게 살펴보면,

0:004> !dumpheap -mt 000007ff00056ce0
         Address               MT     Size
0000000046f11000 000007ff00056ce0 575949792     
000000007fff1000 000007ff00056ce0 1151900496     
total 0 objects
Statistics:
              MT    Count    TotalSize Class Name
000007ff00056ce0        2   1727850288 System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
Total 2 objects

이제 주어진 Address 필드 값을 가지고 좀 더 자세히 보면,

0:004> !dumpobj 0000000046f11000
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
MethodTable: 000007ff00056ce0
EEClass:     000007ff00056c38
Size:        575,949,792(0x22544be0) bytes
Array:       Rank 1, Number of elements 23997907, Type VALUETYPE
Element Type:System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]]
Fields:
None

0:004> !dumpobj 000000007fff1000
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
MethodTable: 000007ff00056ce0
EEClass:     000007ff00056c38
Size:        1,151,900,496(0x44a89b50) bytes
Array:       Rank 1, Number of elements 47995853, Type VALUETYPE
Element Type:System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]]
Fields:
None

"HashSet`1+Slot" 타입의 인스턴스가 2개 할당되어 있고, 각각 용량이 575,949,792(0x22544be0)bytes, 1,151,900,496(0x44a89b50)bytes로 되어 있는 것을 확인할 수 있습니다. 아니? 방금 전에는 개체 하나가 2GB로 제한되어 있어서 예외가 발생한다고 말했으면서 결과는 이와 다르니... 살짝 난감합니다. ^^;

자,,, 어쨌든 결과를 믿어야겠으니... 이야기를 꿰어맞춰 보도록 하겠습니다.

우선 단서가 있다면, HashSet 같은 자료 구조는 내부적으로 용량이 모자랄 때 그 크기를 증가시켜서 재할당한다는 것을... 모든 닷넷 프로그래머들은 잘 아실 것입니다. ^^

오호라... 가만 보니까, 575,949,792bytes는 1,151,900,496bytes의 거의 절반에 가까운 값임을 눈짐작으로 가늠됩니다. 아하~~~ 그럼 설명이 되는군요. 약 500MB짜리의 000000007fff1000 인스턴스가 위에서 보이는 것은 GC가 되기 전이어서 살아 남아 있는 거라고 설명이 될 것 같고, 그러하니 현재 HashSet 인스턴스는 1GB짜리의 000000007fff1000가 될 것입니다. 그 용량이 모자라서 다시 Heap에 2배수에 가까운 메모리를 할당하려는 데, 하필 그 용량이 2GB를 넘었기 때문에 OutOfMemoryException 예외가 발생했다고... 이야기를 엮을 수 있겠습니다. (휴~~~ 왠지 들어맞는 것 같아서 다행입니다. ^^)

신빙성을 더하기 위해 소스 코드를 한번 들여다 볼까요? HashSet의 Add -> AddIfNotPresent 메서드를 거쳐서,

public bool Add(T item)
{
    return this.AddIfNotPresent(item);
}

private bool AddIfNotPresent(T value)
{
    int freeList;
    ...[생략]...
            this.IncreaseCapacity();
            index = hashCode % this.m_buckets.Length;
    ...[생략]...
    return true;
}

IncreaseCapacity 메서드를 들여다 보니... 답이 나왔습니다.

private void IncreaseCapacity()
{
    int min = this.m_count * 2;
    if (min < 0)
    {
        min = this.m_count;
    }
    int prime = HashHelpers.GetPrime(min);
    if (prime <= this.m_count)
    {
        throw new ArgumentException(SR.GetString("Arg_HSCapacityOverflow"));
    }
    Slot<T>[] destinationArray = new Slot<T>[prime];
    ...[생략]...
}

위의 소스 코드를 보니, DumpHeap 결과의 "System.Collections.Generic.HashSet`1+Slot"에서 왜 "Slot"이 들어갔는지도 설명이 되는군요.




문제는 일단 확인했고, 그렇다면 이를 극복하려면 어떻게 해야할까요?

역시 이에 대한 우회적인 해법도 소개해드린 글에서 제시하고 있습니다.

  1. x64의 사용자 영역에 할당가능한 메모리가 8TB인데, 과연 메모리가 그만큼 허용된다고 해서 하나의 컬렉션에서 시스템 전체의 심각한 성능 저해를 유발시키면서까지 메모리를 소비하는 것이 바람직한지를 심사숙고하시고, 응용 프로그램 설계를 다시 적절하게 하시길 바람!
  2. [또는] 역시 이런 경우의 해법은 C/C++임! P/Invoke 또는 C++/CLI를 이용해서 해결하세요.
  3. [또는] 어차피 하나의 CLR 개체가 2GB 제한이 있는 것일 뿐, 응용 프로그램 수준에서는 제한이 없으므로 차라리 여러 개의 CLR 개체에 분배해서 사용하세요.

이 중에서 2번 항목을 선택해서 C++/CLI 예를 하나 들어볼까요?
기본 데이터 타입인 경우에는 어렵지 않습니다. 아래와 같이, C++에서 지원되는 자료 구조로 값을 넣어주는 래퍼 함수 정도만 정의하면 됩니다.

#pragma once

#include <atlcoll.h>
using namespace ATL;

using namespace System;
using namespace System::Collections;

namespace ClassLib 
{
    public ref class TestHash
    {
    public:
        TestHash(void)
        {
            test = new CAtlMap<__int64, bool>();
        }

        bool Add(__int64 value)
        {
            CAtlMap<__int64, bool>::CPair *found = test->Lookup(value);
            if (found == NULL)
            {
                test->SetAt(value, true);
                return true;
            }

            return false;
        }

    protected:
        ~TestHash()
        {
            delete test;
        }

    private:
        CAtlMap<__int64, bool> *test;
    };
}

그런 후에, C++/CLI 프로젝트를 C#에서 바로 참조하고 다음과 같이 코드를 추가하시면 끝!

ClassLib.TestHash th = new ClassLib.TestHash();

for (long i = 0; i < Int32.MaxValue; i++)
{
    th.Add(i);
}

C++/CLI가 나름 유용할 만한 사례가 하나 추가되는군요. ^^ (소스 코드 첨부)

(위의 C++/CLI 소스코드 빌드하느라고, 지난번 글들(빌드 오류 1, 빌드 오류 2)가 씌여진 것입니다. ^^)





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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/22/2023]

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

비밀번호

댓글 작성자
 



2011-10-19 05시55분
[정세일(spowner)] 평소에 기고하신 글 잘 읽습니다. ^^
[guest]

... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12580정성태3/28/20218767오류 유형: 708. SQL Server Management Studio - Execution Timeout Expired.
12579정성태3/28/20218870오류 유형: 707. 중첩 가상화(Nested Virtualization) - The virtual machine could not be started because this platform does not support nested virtualization.
12578정성태3/27/20219121개발 환경 구성: 560. Docker Desktop for Windows 기반의 Kubernetes 구성 (2) - WSL 2 인스턴스에 kind가 구성한 k8s 서비스 위치
12577정성태3/26/202111185개발 환경 구성: 559. Docker Desktop for Windows 기반의 Kubernetes 구성 - WSL 2 인스턴스에 kind 도구로 k8s 클러스터 구성
12576정성태3/25/20218958개발 환경 구성: 558. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성 (2) - k8s 서비스 위치
12575정성태3/24/20218045개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/23/202112035.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태3/22/20219880개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어 [1]
12572정성태3/22/20219397.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태3/21/20218600오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/202112907개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/202111779개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/20217385오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
12567정성태3/17/20218782개발 환경 구성: 553. Docker Desktop for Windows를 위한 k8s 대시보드 활성화 [1]
12566정성태3/17/20219082개발 환경 구성: 552. Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
12565정성태3/17/20216587오류 유형: 704. curl.exe 실행 시 dll not found 오류
12564정성태3/16/20217049VS.NET IDE: 160. 새 프로젝트 창에 C++/CLI 프로젝트 템플릿이 없는 경우
12563정성태3/16/20219004개발 환경 구성: 551. C# - JIRA REST API 사용 정리 (3) jira-oauth-cli 도구를 이용한 키 관리
12562정성태3/15/202110138개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/20218732VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/202110090개발 환경 구성: 549. ssh-keygen으로 생성한 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/20219492.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/20218971Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/20217624Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/20216904오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/20218963Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...