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 #