12.1 언리얼 엔진의 메모리 관리 시스템
12.1-1 C++ 언어 메모리 관리의 문제점
- C++은 매니지드 언어임. 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트 관리.
따라서 프로그래머가 직접 할당/해지 짝맞추기를 해야 함.
이를 ㅓ지키지 않으면 다양한 문제 발생
- 잘못된 포인터 사용 예시
메모리 누수 / 댕글링 포인터 / 와일드 포인터(주소값이 초기화 되지 않아 엉뚱한 주소를 가리킴)
- 게임 규모가 커지고 복잡해질수록 프로그래머가 실수할 확률은 기하급수적.
모던 개체지향 언어들은 포인터를 버리고 가비지 컬렉션 시스템을 도입.
12.1-2 가비지 컬렉션 시스템
- 프로그램에서 더이상 사용하지 않는 오브젝트를 자동 감지 후 메모리 회수하는 시스템.
동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해서 참조되지 않는 메모리 추적.
- 대부분 Mark-Sweep 방식의 가비지 컬렉션 시스템 사용. 언리얼도 마찬가지.
1. 저장소에서 최초 검색을 시작하는 루트 오브젝트 표기.
2. 루트 오브젝트가 참조하는 개체를 찾아 Mark
3. Mark된 개체로부터 해당 개체가 참조하는 개체를 찾아서 다시 Mark. 계속 반복.
4. 이제 저장소에서 Mark된 개체와 그렇지 않은 개체로 나뉨.
5. 가비지 컬렉터는 저장소에서 Mark 되지 않는 개체(가비지)들의 메모리 회수(Sweep)
12.1-3 가비지 컬렉션을 위한 개체 저장소
- 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 GUObjectArray
GUObjectArray의 각 요소에는 플래그 정보가 있음.
- 가비지 컬렉터가 참고하는 주요 플래그
Garbage 플래그: 다른 언리얼 오브젝트로부터 참조가 없어서 회수 예정인 오브젝트
RootSet 플래그: 다른 언리얼 오브젝트로부터 참조가 없어도 회수되지 않는 오브젝트
12.1-4 가비지 컬렉터의 메모리 회수
- 지정된 주기(GCCycle이라고 함)마다 몰아서 없애도록 설정되어 있음.
Project Settings > Garbage Collection에서 확인 가능.
성능 저하를 막기 위해서 병렬처리, 클러스터링과 같은 기능을 탑재함.
- Garbage 플래그가 Set된 오브젝트를 파악하고 메모리를 안전하게 회수함.
Garbage 플래그는 수동으로 설정하는 것이 아닌, 시스템이 알아서 설정함.
그래서 한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함.
12.1-5 루트셋 플래그의 설정
- AddToRoot() 함수를 호출해 루트셋 플래그를 설정하면 최초 탐색 목록으로 설정됨.
루트셋으로 설정된 언리얼 오브젝트는 메모리 회수로부터 보호받음.
RemoveFromRoot() 함수를 호출해서 루트셋 플래그를 제거할 수도 있음.
다만 컨텐츠 관련 오브젝트에 루트셋을 설정하는 방법은 권장되지 않음.
12.1-6 언리얼 오브젝트를 통한 포인터 문제의 해결
- 메모리 누수 문제
언리얼 오브젝트는 가비지 컬렉터를 통해 자동으로 해결됨.
- 댕글링 포인터 문제
언리얼 오브젝트는 이를 탐지하기 위한 함수를 제공함 ::IsValid()
nullptr인지 아닌지만 확인하면 댕글링 포인터 문제가 발생할 수 있음.
- 와일드 포인터 문제
언리얼 오브젝트에 UPROPERTY() 속성을 지정하면 자동으로 nullptr로 초기화 해줌.
12.1-7 회수되지 않는 언리얼 오브젝트
- 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트
UPROPERTY()로 참조된 언리얼 오브젝트(대부분 이 방식을 사용)
AddReferencedObject() 함수를 통해 참조를 설정한 언리얼 오브젝트
- 루트셋으로 지정된 언리얼 오브젝트
- 기본 원칙
언리얼 오브젝트 포인터는 가급적 UPROPERTY()로 선언. 그외의 경우엔 뒤에 나옴.
메모리는 가비지 컬렉터가 자동으로 관리하도록 위임.
생성된 언리얼 오브젝트는 강제로 지우려하지 말것. 참조를 끊는다는 생각으로 설계.
다만 가비지 컬렉터에게 회수를 재촉할 수는 있음.(ForceGarbageCollection() 함수)
12.1-8 일반 클래스에서 언리얼 오브젝트를 관리하는 경우
- UPROPERTY()를 사용하지 못하는 일반 C++ 클래스가 언리얼 오브젝트를 관리해야하는 경우
FGCObject 클래스를 상속 받은 후 AddReferencedObject() 함수를 구현함.
함수 구현부에서 관리할 언리얼 오브젝트를 추가해줌.
시스템을 구축하면서 필요한 상황이 발생할 수 있음.
12.1-9 언리얼 오브젝트 메모리 실습
- 프로젝트 설정에서 가비지 컬렉션 GCCycle을 3초로 단축.
- 새로운 GameInstance의 두 함수를 오버라이드
Init(): 프로그램이 초기화될 때 호출됨.
Shutdown(): 프로그램이 종료될 때 호출됨.
- 테스트 시나리오
플레이 버튼을 누를 때 Init() 함수에서 오브젝트 생성
3초 이상 대기 후 가비지 컬렉션 동작을 기다림.
플레이 중지 버튼을 눌러 Shutdown() 함수에서 생성한 오브젝트의 유효성을 확인.
- 새 C++ 클래스 > None 부모 클래스 > "StudentManager" 클래스 생성
<hide/>
// StudentManage.h
#pragma once
#include "CoreMinimal.h"
class UNREALMEMORY_API FStudentManager : public FGCObject
{
public:
FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
virtual FString GetReferencerName() const override
{
return TEXT("FStudentManager");
}
const class UStudent* GetStudent() const { return SafeStudent; }
private:
class UStudent* SafeStudent = nullptr;
};
<hide/>
// StudentManage.cpp
#include "StudentManager.h"
#include "Student.h"
void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
if (SafeStudent->IsValidLowLevel())
{
Collector.AddReferencedObject(SafeStudent);
}
}
<hide/>
// MyGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
UCLASS()
class UNREALMEMORY_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
virtual void Shutdown() override;
private:
TObjectPtr<class UStudent> NonPropStudent;
UPROPERTY()
TObjectPtr<class UStudent> PropStudent;
TArray<TObjectPtr<class UStudent>> NonPropStudents;
UPROPERTY()
TArray<TObjectPtr<class UStudent>> PropStudents;
class FStudentManager* StudentManager = nullptr;
};
<hide/>
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "StudentManager.h"
void CheckUObjectIsValid(const UObject* InObject, const FString& InTag)
{
if (InObject->IsValidLowLevel())
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효한 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
}
}
void CheckUObjectIsNull(const UObject* InObject, const FString& InTag)
{
if (nullptr == InObject)
{
UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터가 아닌 언리얼 오브젝트"), *InTag);
}
}
void UMyGameInstance::Init()
{
Super::Init();
NonPropStudent = NewObject<UStudent>();
PropStudent = NewObject<UStudent>();
NonPropStudents.Add(NewObject<UStudent>());
PropStudents.Add(NewObject<UStudent>());
StudentManager = new FStudentManager(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
const UStudent* StudentInManager = StudentManager->GetStudent();
delete StudentManager;
StudentManager = nullptr;
CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager"));
CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager"));
CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents"));
CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents"));
}
'Unreal > [서적] 언리얼5 이득우님 인프런1' 카테고리의 다른 글
Ch 14. 패키지 (0) | 2023.05.13 |
---|---|
Ch 13. 직렬화 (0) | 2023.05.13 |
Ch 11. 구조체와 Map (0) | 2023.05.13 |
Ch 10. Array and Set (0) | 2023.05.12 |
Ch 09. 델리게이트 (0) | 2023.05.12 |
댓글