힙 메모리

 힙은 크기가 작은 데이터 블록을 할당 하는데 매우 유용한 방법으로 가상 메모리와 달리 할당 단위나 페이지 경계와

 같은 특성을 고려할 필요 없이 메모리 할당 할 수 있습니다.

 대신에 힙 매니져에 의해서 메모리 할당이 진행되기 때문에 물리적 저장소를 커밋하거나 디커밋하는 직접적인

 제어권을 제어 할 수 없고 수행 속도가 느립니다. 

 만약에 사용자가 메모리 운용 방식을 제어하고 싶으면 가상 메모리 함수(VirtualAlloc, VirtualFree)를 사용해야 합니다.

프로세스의 기본 힙(Default Heap)

함 수 설  명
HANDLE GetProcessHeap() 해당 함수를 호출하면 기본힙에 대한 핸들을 얻을 수 있습니다. 

 프로세스가 초기화 되면 시스템은 프로세스의 주소 공간 내에 힙을 생성합니다. 이 힙을 프로세스의

 기본 힙(Default Heap) 이라고 합니다. 기본 힙은 크기는 1MB의 영역을 예약하는데 시스템은 사용하면

 힙의 크기를 증가시켜서 사용 할 수 있습니다. 

 스레드 스택과 마찬가지로 링커스위치를 사용해서 힙의 시작 크기를 변경 할 수 있습니다. 

 많은 수의 윈도우 함수들이 내부적으로 메모리 공간이 필요하면 기본 힙을 통해서 메모리를 할당합니다. 

힙과 스레드 동기화

힙의 스레드 동기화

 힙 메모리는 프로세스에서 공유되는 자원이기 때문에 스레드가 동시에 접근 할 수 있는 문제가 있습니다.

 기본 힙이나 새로 생성되는 힙에 옵션 없이 생성하게 되면 힙에서 메모리를 할당 할 때 내부적으로 

 스레드 동기화 작업이 진행 됩니다.

힙 관련 함수 사용 방법

힙 오브젝트 생성 함수

// 힙을 생성합니다. 
// @Return : 성공시 힙 오브젝트 핸들, 실패시 NULL
HANDLE HeapCreate(DWORD fdwOption, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
인 자 값 설  명
fdwOptions

힙 생성시 동작방식에 대한 플래그를 설정합니다.

HEAP_NO_SERIALIZE : 힙에서 메모리 할당시 내부에서 수행하는 스레드 동기화 작업 수행하지 않도록 한다.

HEAP_GENERATE_EXCEPTIONS : 힙에서 메모리 할당이나 재할당 실패시 EXCEPTION을 발생 시킴

HEAP_CREATE_ENABLE_EXECUTE : 힙에 수행할 수 있는 코드를 저장할 때 이 옵션을 사용한다.

dwInitialSize 힙 생성시 커밋 할 바이트 수를 지정합니다. 
dwMaximumSize 힙의 최대 확장 크기를 지정합니다. 0을 사용하면 제한이 없는 힙이 생성됩니다. 
// 힙 오브젝트를 해제합니다.
// @Return : 성공시 TRUE, 실패시 FALSE
BOOL HeapDestroy(HANDLE hHeap);

 HeapCreate 함수를 호출하면 추가적인 힙 오브젝트를 생성할 수 있습니다.

 사용을 마친 힙 오브젝트는 HeapDestroy 함수를 통해서 해제 할 수 있습니다. 힙 오브젝트를 해제 하면 힙 내의 

 모든 메모리 블록들은 자동적으로 해제 되며 힙에서 사용하던 물리적 저장소와 메모리 영역은 해제되며 시스템에 

 반납됩니다.

 만약의 사용자가 힙을 해제하지 않는다면 프로세스가 종료될 때 시스템이 자동적으로 해제 합니다.

 프로세스의 기본 힙은 사용자가 해제 할 수 없습니다. 

예제코드를 통한 설명


// 기본 힙 핸들
HANDLE hDefaultHeap = GetProcessHeap();
// 새로운 힙 핸들
HANDLE hNewHeap = HeapCreate(NULL, 1024, 0);
if (hNewHeap)
{
	// 새로운 힙 핸들 제거
	if (TRUE == HeapDestroy(hNewHeap)) hNewHeap = NULL;
}

디버그 모드로 실행한 힙의 메모리 주소
hNewHeap 생성시 메모리 할당 정보

 실제로 위의 구문을 디버그 모드로 순차적으로 실행해서 메모리를 확인해보면 힙 오브젝트이 핸들 값 자체가 

 실제 메모리 주소를 가지고 있는 것을 확인 할 수 있습니다. 해당 메모리 위치로 찾아가보면

 새로 할당한 힙은 0x000001b55e490000 ~ x000001b55e492000 ( 2 페이지 ) 범위의 메모리가 커밋 되어 있는 것을

 확인 할 수 있습니다. (기본 힙은 24페이지가 커밋 되어 있습니다.)

 힙으로 부터 메모리를 할당하면 커밋된 메모리 구역에서 비어 있는 자리를 힙 매니저가 할당해서 요청자에게

 전달합니다. 만약에 공간이 모자르면 메모리를 더 커밋하여 확장합니다. 

hNewHeap 해제시 메모리 정보

 힙을 사용을 다한 후에 HeapDestroy함수를 호출하면 할당된 모든 메모리가 반환 된 것을 디버그 메모리 상태를 보고

 확인 할 수 있습니다. HeapDestroy함수가 성공하면 Heap핸들 객체를 꼭 NULL 초기화 시켜서 잘못된 접근이 발생하지 

 안도록 주의합니다. 

힙으로부터 메모리 할당 및 해제 함수

// 힙에서 메모리 할당 방법
// @return : 성공시 메모리 블록 주소, 실패시 NULL
HANDLE HeapAlloc(HANDLE hHeap, DWORD fdwFlags, SIZE_T dwBytes);
인 자 값 설  명
hHeap 어떤 힙으로 부터 메모리를 할당할지 지정합니다.  GetProcessHeap()를 사용하면 기본힙을 사용 할 수 있습니다. 
fdwFlag

메모리 할당 방식의 영향을 주는 플레그를 전달합니다. 

HEAP_ZERO_MEMORY : 할당하는 메모리 블록의 내용를 0으로 채웁니다.

HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS 위와 같다.

dwBytes 힙으로 할당할 메모리의 크기를 지정합니다.
// 메모리 블록을 해제합니다
// @return : 성공시 TRUE, 실패시 FALSE
BOOL HeapFree(HANDLE hHeap, DWORD fdwFlags, PVOID pvMem);

HeapAlloc함수를 사용하면 힙으로부터 메모리를 할당합니다.

내부적으로 힙 매니저에 의해서 아래의 로직이 수행됩니다. 

  1. 요청된 크기를 수용할 수 있는 비어 있는 메모리 블록을 찾습니다.
  2. 비어 있는 메모리 블록에 할당 되었음을 표시합니다. 
  3. 메모리 블록 링크드 리스트에 추가합니다.

 위의 작업은 스레드에 의해서 동시 접근이 가능하기 때문에 내부적으로 순차적인 접근만을 허용합니다.

 (HeapLock, HeapUnLock 함수를 사용해서 구현합니다.)

 HEAP_NO_SERIALIZE옵션을 사용하게 되면 내부적으로 스레드 동기화 작업을 하지 않는데

 꼭 안정성이 보장될 때만 사용 해야 합니다.

 안정성이 보장되기 위해서는 힙을 단일 스레드에서만 접근되거나 힙에 대한 접근을 하기 전에 동기화 함수를 사용해서 

 안전성을 보장할 때입니다.

 (웬만하면 해당 플레그를 전달하지 않는 것이 좋습니다. 현재는 스레드 안정성이 보장된다 하더라고 소스코드는 재사용 및 수정이 발생되기 때문에 미래의 일까지 예측하는건 불가능하기 때문입니다.)

 사용을 다한 메모리는 HeapFree를 통해서 해제 할 수 있습니다. 

예제코드를 통한 설명

HANDLE hNewHeap = HeapCreate(NULL, 1024, 0);

if (hNewHeap)
{
    LPVOID pAllocMem = HeapAlloc(hNewHeap, HEAP_ZERO_MEMORY, sizeof(DWORD));

    if (pAllocMem)
    {
        DWORD* pdwNewData = static_cast<DWORD*>(pAllocMem);
        *pdwNewData = 0x1000;

        HeapFree(hNewHeap, NULL, pAllocMem);
    }

    if (TRUE == HeapDestroy(hNewHeap))
        hNewHeap = NULL;
}

힙에서 메모리 할당시 메모리 정보
할당된 메모리 값

 예제 코드를 보면 hNewHeap라는 힙을 생성했고 힙으로 부터 pAllocMem 메모리를 할당 받고 DWORD로 변환해서 

 0x1000이라는 값을 대입했습니다. 메모리 정보를 보면 할당된 메모리는 힙의 메모리 범위인

 0x0000028383190000 ~ 0x0000028383192000 사이값을 가지는 것을 확인 할 수 있습니다.

HeapFree후 해제 될때 메모리 값

 HeapFree 함수를 호출하면 해당 메모리를 해제하면서 메모리가 해제되었다고 표시합니다. 

// 힙에서 할당한 메모리 블록의 크기 변경 
// @return : 재할당 성공하면 메모리 블록 주소, 실패하면 NULL
HANDLE HeapReAlloc(HANDLE hHeap, DWORD fdwFlags, SIZE_T dwBytes);
인 자 값 설  명
hHeap 위의 내용과 같습니다. 메모리 할당할 힙 핸들
fdwFlags

HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS 위와 같고

HEAP_ZERO_MEMORY : 메모리가 증가된 부분만 0으로 설정합니다.

HEAP_REALLOC_IN_PLACE_ONLY : 재할당시 메모리 크기가 커지는 경우에 힙 매니저가 메모리 블록의 위치를 변경하려고 할 수도 있는데 해당 플레그를 전달하면 재할당하는 메모리 블록을 옮기지 못하도록 합니다.

pvMem 재할당할 메모리 블록의 현재 주소를 전달합니다.
dwBytes 새롭게 할당하는 메모리 블록 크기를 전달합니다. 

 해당 함수를 사용하면 할당된 메모리의 크기를 변경 할 수 있습니다. 메모리 블럭의 크기를 변경하면서 메모리 블록의

 주소가 변경될 수도 있습니다.

 예를 들어 재할당을 요청했는데 현재의 힙에는 요청한 크기의 비어 있는 공간이 없다면 힙은 새로운 공간을 커밋해서

 메모리를 할당하게 되고 메모리 블록의 주소가 변경됩니다.

// 메모리 블록의 크기를 가져옴
// @return : 해당 블록의 크기를 리턴합니다.
SIZE_T HeapSize(HANDLE hHeap, DWORD fdwFlags, LPCVOID pvMem);

 해당 함수를 사용하면 할당 된 메모리 블록의 크기를 전달 받을 수 있습니다.

// 힙의 무결성을 확인 합니다.
// @return : 오류가 없으면 TRUE, 오류가 있으면 FALSE
BOOL HeapValidate(HANDLE hHeap, DWORD fdwFlags, LPCVOID pvMem);

 힙 내의 모든 블록을 검사해서 손상 여부를 확인합니다.

 pvMem에 특정 블록의 주소를 입력하면 해당 블록만 검사합니다.

개별적인 힙(Private Heap) 생성 이점

 프로세스는 기본 힙과는 별도로 사용할 수 있는 추가적인 힙을 생성할 수 있습니다.

 프로세스에서 기본 힙이 아닌 개별적인 힙을 사용하면서 얻을 수 있는 이점에 대해서 알아 봅니다. 

개별적인 힙 사용시 메모리 구조

 예를 들어서 애플리케이션에서 LinkedList를 위한 LinkNode 구조체와 BinaryTree를 위한 BRANCH 구조체를 동시에

 처리하기 위해서 기본힙을 사용할 때와 개별적인 힙을 사용할 때의 메모리 구조를 그림으로 나타내었습니다

컴포넌트 보호

메모리 오버플로우 발생시

 만약에 예제그림의 메모리 상태의 기본 힙 구조에서 LinkedList에서 버그가 발생해서 NODE2위치에서 메모리 오버

 플로우가 발생된다면 전혀 상관이 없는 BinaryTree의 Branch1의 구조체를 접근할 때 에러가 발생될 것입니다. 

 이러한 에러의 원인은 매우 발견하기 어렵습니다. 하지만 두개의 개별적인 힙을 통해서 메모리 할당 위치를 분리해두면

 서로 간 버그로 인해서 간섭되는 현상을 방지 할 수 있습니다. 

 효율적인 메모리 관리

 힙 메모리는 동일 크기의 오브젝트를 할당 할 때 더욱 더 효율적으로 관리 될 수 있습니다. 

 예제에서 NODE 구조체의 크기가 24바이트고 BRANCH의 구조체가 40바이트라고 한다면 할당되는 오브젝트에 크기가

 다르기 때문에 메모리 단편화가 발생됩니다. NODE 구조체가 하나만 해제 된 자리에는 BRANCH 구조체가 들어 갈 수

 없습니다. 또 BRANCH 구조체가 해제 된 자리에 NODE 구조체가 들어 간다면 16바이트의 빈 공간이 생깁니다. 

 같은 사이즈의 오브젝트만 관리되는 힙이라면 이러한 문제가 발생되지 않습니다. 

 추가적으로 애플리케이션을 설계할 때 함께 사용되는 자료들을 가까운 곳에 할당 하는 것이 좋은 설계 방식입니다. 

 관계있는 모든 데이터가 최소한의 페이지 크기에 연속적인 메모리에 담겨져 있다면 효율적으로 동작할 수 있습니다.

 (램과 페이징 파일간의 최소한의 Swap이 발생됩니다.)

스레드 동기화 비용 회피

 힙은 프로세스 내에서 공유되는 자원이기 때문에 특정 시점에 동일 힙에 대해서 여러 스레드가 동시에 접근하면 

 스레드 동기화로 인해서 성능에 좋지 않은 영향을 미칩니다. 힙을 여러개를 만들어서 분산한다면 동기화 문제에

 대해서 조금 더 빠르게 동작 할 수 있습니다. 

 혹시라도 하나의 스레드에서만 힙에 접근하는 상황이라면 스레드 동기화 작업을 하지 않는 힙을 생성해서 

 성능상 이점을 가질 수 있습니다.

 하지만 주의가 필요합니다. 여러사람에 의해서 만들어지는 코드들은 어떻게 변경될 지 예측할 수 없기 때문에

 잠재적인 버그 요소가 될 수 있습니다.

C++ 에서 힙 메모리 사용

 c++에서는 new delete를 사용해서 메모리를 할당하는데 해당 메모리는 어떻게 할당하는 것일까?

// 프로세스의 기본 힙
HANDLE hDefaultHeap = GetProcessHeap();
// 힙에서 메모리 할당
LPVOID pHeapAllocMem = HeapAlloc(hDefaultHeap, HEAP_ZERO_MEMORY, sizeof(DWORD));
// c++ runtime new로 메모리 할당
DWORD* pdwNewAllocMem = new DWORD;

 위의 로직을 실행시켜 보시면 기본힙의 할당 범위는 0x0000020AD4220000 ~ 0x0000020AD423A000이고

 new에서 할당한 메모리도 기본힙에서 할당받는다는 것을 알 수 있습니다.

 ( new를 호출하면 생성자를 호출해준다. WIN API의 힙 함수로 호출하면 생성를 호출하지 않는다.)

 c++에서 개별적인 힙의 장점을 사용할 수 없을까?

// 개별적인 Heap 사용하는 클래스입니다. 
class CPrivateHeapObj
{
public:
    CPrivateHeapObj()
    {
        cout << "Construct CPrivateHeapObj" << endl;
    }
    ~CPrivateHeapObj()
    {
        cout << "destruct CPrivateHeapObj" << endl;
    }

    void* operator new(size_t size)
    {
        if (NULL == s_hHeap)
        {
            s_hHeap = ::HeapCreate(NULL, 0, 0);

            if (s_hHeap == NULL)
                return nullptr;
        }

        void * p = ::HeapAlloc(s_hHeap, 0, size);
        if (nullptr != p)
        {
            s_dwNumAllocInHeap++;
        }
        return (p);
    }
    void operator delete(void* p)
    {
        if (::HeapFree(s_hHeap, 0, p))
        {
            s_dwNumAllocInHeap--;
        }

        if (s_dwNumAllocInHeap == 0)
        {
            if (::HeapDestroy(s_hHeap))
                s_hHeap = NULL;
        }
    }
private:
    static HANDLE s_hHeap;
    static DWORD s_dwNumAllocInHeap;
};

HANDLE CPrivateHeapObj::s_hHeap = NULL;
DWORD CPrivateHeapObj::s_dwNumAllocInHeap = 0;
    // 프로세스의 기본 힙
    HANDLE hDefaultHeap = GetProcessHeap();
    // 힙에서 메모리 할당
    LPVOID pHeapAllocMem = HeapAlloc(hDefaultHeap, HEAP_ZERO_MEMORY, sizeof(CPrivateHeapObj));
    // 개별적인 힙을 사용하는 오브젝트
    // 주소를 확인해보면 다른 힙을 사용하는 것을 확인 할 수 있습니다.
    CPrivateHeapObj* pPHO = new CPrivateHeapObj;
    delete pPHO;

 위의 예제는 CPrivateHeapObj객체의 new와 delete 연산자를 오버로딩해서 개별적인 힙에서 메모리를 할당하도록 

 구현하였습니다. 

 

 Heap관련 MSDN 주소를 링크 해놓습니다. 

 https://docs.microsoft.com/en-us/windows/win32/api/heapapi/

 

Heapapi.h header - Win32 apps

01/11/2019 2 minutes to read In this article --> This header is used by System Services. For more information, see: Functions Title Description GetProcessHeap Retrieves a handle to the default heap of the calling process. GetProcessHeaps Returns the number

docs.microsoft.com

 

+ Recent posts