Skip to main content

[Unreal] 언리얼 메모리 관리, GC, 스마트 포인터




C++ 포인터의 메모리 관리 문제점 #

  • 메모리 누수(Leak)
    • new 를 했지만 delete 를 까먹어서 힙에 메모리가 그대로 남아 있는 문제.
    • → 언리얼은 가비지 컬렉터가 자동으로 해제해준다.
int* ptr = new int(5);
// delete ptr로 메모리를 해제하지 않음. 

  • 댕글링(Dangling) 포인터
    • 다른 곳에서 이미 해제해서 무효화된 오브젝트의 주소를 가리키게 되는 문제.
    • → 언리얼은 ::IsValid()로 탐지 가능하다.
int* ptr = new int(3);
cout << *ptr << endl; // 3 -> OK

delete ptr;
cout << *ptr << endl; // -559054800 -> NOT OK

  • 와일드(Wild) 포인터
    • 값이 초기화되지 않아서 포인터가 엉뚱한 주소를 가리키는 문제.
    • → 언리얼은 UPROPERTY()를 붙이면 자동으로 nullptr로 초기화해준다.
int* ptr;
cout << *ptr << endl; // 137164184 -> NOT OK



  • 이런 문제들을 해결하기 위해서 가비지 컬렉션 시스템을 도입했다.

언리얼의 가비지 컬렉션 시스템 #

  • 가비지 컬렉션 시스템(GC)
    • 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해서 메모리를 회수하는 시스템이다.
  • 언리얼은 마크&스윕(Mark-and-Sweep) 방식의 가비지 컬렉션을 사용한다.

  • 관리되는 모든 언리얼 오브젝트 정보는 전역 변수인 GUObjectArray에 저장된다.
    • 배열의 각 요소에는 Flag가 함께 저장되어 있는데, Garbage 플래그가 되면 메모리 회수 대상이 된다.
  • 동작 과정
    • 저장소에서 검색을 처음으로 시작하는 루트 오브젝트(RootSet) 를 표기한다.
    • 루트 오브젝트가 참조하는 객체를 찾아서 마크(Mark) 한다.
    • 마크된 객체로부터 다시 참조하는 객체를 찾아서 마크하고 이것을 반복한다.
    • 이제 저장소에는 마크된 객체와 마크되지 않은 객체로 나뉜다.
    • 가비지 컬렉터가 저장소에서 마크되지 않은 객체(Garbage)들의 메모리를 회수한다. (Sweep)

  • 언리얼은 지정된 주기마다 몰아서 없애도록 설정되어 있다.
    • Project Settings > Garbage Collection > Time Between Purging Pending Kill Objects (GCCycle : 기본값은 60초다)
  • 한 번 생성된 언리얼 오브젝트는 바로 삭제되지 않고, 참조 정보가 없어지면 가비지 컬렉터 동작 시점에 따라 삭제된다.



회수되지 않는 언리얼 오브젝트 #

  • AddToRoot()를 통해 RootSet으로 지정된 언리얼 오브젝트
  • UPROPERTY() 로 참조된 언리얼 오브젝트
  • AddReferencedObject() 함수를 통해 참조를 설정한 언리얼 오브젝트 (아래 설명…)



일반 C++ 클래스에서 언리얼 오브젝트를 관리하는 경우 #

  • 일반 C++ 클래스에서는 UPROPERTY()를 사용하지 못한다.
  • 따라서 FGCObject 를 상속받은 후에 AddReferencedObject()를 구현해준다. 그리고 구현부에 관리할 언리얼 오브젝트들을 추가해준다.
// 일반 C++ 클래스 
class API FStudentManager : public FGCObject // FGCObject을 상속한다. 
{
public:
    //...

    // AddReferencedObject() 를 구현한다.
    virtual void AddReferencedObject(FReferenceCollector& Collector) override;

    // GetReferencerName() 을 구현한다. 
    virtual FString GetReferencerName() const override 
    {
        // 클래스 이름을 넣어주면 된다. 
        return TEXT("FStudentManager");
    }

private:
    // 관리하고 싶은 언리얼 오브젝트 
    class UStudent* SafeStudent = nullptr;
};
void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
    // 메모리에서 유효하면 
    if (SafeStudent->IsValidLowLevel())
    {
        // 참조 관계를 형성해준다. 
        Collector.AddReferencedObject(SafeStudent);
    }
}



::IsValid()가 검사하는 것들 #

  • nullptr 인가?
  • Pending Kill 상태인가? → 곧 수거될 오브젝트를 의미한다.
  • GC에 의해 수거 되었는가?



스마트 포인터 #

  • 언리얼 오브젝트를 GC로 관리한다면, 순수 C++을 사용할 때는 스마트 포인터들을 사용할 수 있다.
  • 스마트 포인터(smart pointer)란 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해준다.
  • 언리얼 공식 문서 : 스마트 포인터 라이브러리

TUniquePtr #

  • 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터이다.
TUniquePtr<FMyObject> UniquePtr1 = TUniquePtr<FMyObject>(MyObject);

// 소유권은 이렇게 옮길 수 있다. 
TUniquePtr<FMyObject> UniquePtr2 = MoveTemp(MyObject);

TSharedPtr #

  • 하나의 객체를 여러개의 포인터가 가리킬 수 있도록 참조 횟수(reference count) 를 도입한 스마트 포인터이다.
  • 대입할 때마다 참조 횟수가 올라가고 스마트 포인터의 수명이 다하면 1씩 줄어들어서 참조 횟수가 0이 되면 자동으로 메모리를 해제한다.
{
    {
        // 참조 횟수: 1
        TSharedPtr<FMyObject> SharedPtr1 = MakeShareable(MyObject);
        
        {
            // 참조 횟수: 2
            TSharedPtr<FMyObject> SharedPtr2 = MyObject;
            
            // 참조 횟수: 3
            TSharedPtr<FMyObject> SharedPtr3 = MyObject;
        }
        // SharedPtr2 소멸 -> 참조 횟수: 2
        // SharedPtr3 소멸 -> 참조 횟수: 1
        
    }    
    // SharedPtr1 소멸 -> 참조 횟수: 0 -> 메모리 해제
}

TWeakPtr #

  • TSharedPtr을 통해서만 대입이 가능하며, 객체를 참조하려면 TSharedPtr로 변환하여 사용해야 한다.
  • TSharedPtr이 가리키는 객체에 접근할 수 있지만, 참조 횟수를 증가시키지 않는다. 즉, 객체에 대한 약한 참조(weak reference) 를 유지한다.
  • TSharedPtr은 순환 참조(circular reference) 문제가 발생할 수 있다. 객체가 서로를 참조하면 참조 횟수가 0이 되지 않으므로 메모리가 해제되지 않는 이다. 이때 TWeakPtr이 진가를 발휘할 수 있다.
{
    {
        // 참조 횟수: 1
        TSharedPtr<FMyObject> SharedPtr1(MyObject);

        // 참조 횟수: 1
        TWeakPtr<FMyObject> WeakPtr = SharedPtr1; // TSharedPtr로부터만 복사 가능하다.

        {
            // 참조 횟수: 2
            TSharedPtr<FMyObject> SharedPtr2 = WeakPtr.Pin(); // TSharedPtr로 변환해서만 사용 가능하다.
        }

        // SharedPtr2 소멸 -> 참조 횟수: 1
    }
    // SharedPtr1 소멸 -> 참조 횟수: 0 -> 메모리 해제
}



References #