다양한 기록

[UE] Memory Management 본문

언리얼 엔진/Unreal C++

[UE] Memory Management

라구넹 2024. 12. 29. 17:58

기존 C++ : new, delete 해줘야 함

-> 누수, 댕글링 포인터, 초기화되지 않은 포인터 등

문제가 많이 발생함

 

가비지 컬렉션 시스템

- 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동 감지하여 회수

- 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리 추적

- 마크-스윕(Mark-Sweep) 방식의 가비지 컬렉션

    - 저장소에서 최초 검색을 시작하는 루트 오브젝트 표기

    - 루트 오브젝트가 참조하는 객체를 찾아 마크

    - 마크된 객체에서 계속해서 참조하는 객체를 찾아 마크(1로 표시)

    - 마크되지 않은 객체(0으로 표시) -> 회수(스윕)

 

언리얼 엔진에선 주기적으로 알아서 작동

기본 값 60초, 부하가 좀 있는 작업인데 병렬 처리 및 클러스터링 기능 탑재

 

가비지 컬렉터를 위한 객체 저장소

- 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수: GUObjectArray

- GUObjectArray의 각 요소는 플래그를 가짐

- Garbage 플래그: 참조가 없어 회수 예정

- RootSet 플래그: 참조가 없어도 회수하지 않는 오브젝트 (최초라 보면 됨)

 

메모리 회수

- 지정된 시간에 따라 메모리 회수

- 가비지 플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수

- 가비지 플래그는 시스템이 알아서 설정함

- 어떤 오브젝트가 너무 중요해서 사라지면 안된다 -> 루트셋으로 등록하면 됨

- AddToRoot 함수를 호출해 루트셋 플래그 설정 가능

- RemoveFromRoot 호출하여 루트셋 플래그 제거 가능

 

장점

- 메모리 누수 안됨 (알아서 해줌)

- 댕글링 포인터 (IsValid 같은 탐지용 함수를 제공함)

- 와일드 포인터 (UPROPERTY로 지정하면 자동 nullptr 초기화)

 

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

- UPROPERTY로 참조된 언리얼 오브젝트

- AddReferencedObject 함수를 통해 참조를 설정한 언리얼 오브젝트

- 루트셋으로 지정된 언리얼 오브젝트 (많이 안 씀)

* C++ 객체 내에 언리얼 오브젝트가 멤버로 들어가면 두번째 방법을 써야 함

* 이때, 언리얼 오브젝트가 FGCObject를 상속받고 AddReferencedObject 구현해주면 됨

 

주의 사항

생성된 언리얼 오브젝트는 강제로 지우려 하지 말 것

- 참조를 끊는다는 생각

- 가비지 컬렉터에게 회수 재촉은 가능 (ForceGarbageCollection 함수)

- 콘텐츠 제작에서 Destroy 함수 사용도 가능한데 이것도 결국 가비지 컬렉터가 나중에 처리해야 함

 

 

// Fill out your copyright notice in the Description page of Project Settings.

#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;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstance.h"
#include "Student.h"
#include "StudentManager.h"

void CheckUObjectIsValid(const UStudent* 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 UStudent* InObject, const FString& InTag)
{
	if (InObject == nullptr)
	{
		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>(), NewObject<UStudent>());

	UE_LOG(LogTemp, Log, TEXT("===== Init ===="));
}

void UMyGameInstance::Shutdown()
{
	Super::Shutdown();

	const UStudent* SafeStudentInManager = StudentManager->GetSafeStudent();
	const UStudent* UnSafeStudentInManager = StudentManager->GetUnSafeStudent();

	delete StudentManager;
	StudentManager = nullptr;

	CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));
	CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
	CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
	CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));

	CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents"));
	CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents"));
	CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents"));
	CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents"));

	CheckUObjectIsValid(UnSafeStudentInManager, TEXT("UnSafeStudentInManager"));
	CheckUObjectIsNull(UnSafeStudentInManager, TEXT("UnSafeStudentInManager"));

	CheckUObjectIsValid(SafeStudentInManager, TEXT("SafeStudentInManager"));
	CheckUObjectIsNull(SafeStudentInManager, TEXT("SafeStudentInManager"));
}

IsValidLowLevel은 객체의 메모리 구조가 유효한지를 체크

UPROPERTY로 지정된 객체는 회수가 안되고,

 FGCObject를 구현하여 설정한 경우에도 회수가 안됨, 그걸 확인해보는 코드

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

/**
 * 
 */
class UNREALMEMORY_API FStudentManager : FGCObject
{
public:
	FStudentManager(class UStudent* InSafeStudent, class UStudent* InUnSafeStudent) : 
		SafeStudent(InSafeStudent), UnSafeStudent(InUnSafeStudent) {}
	const class UStudent* GetSafeStudent() const { return SafeStudent; }
	const class UStudent* GetUnSafeStudent() const { return UnSafeStudent;  }

	// FGCObject 구현
	virtual void AddReferencedObjects( FReferenceCollector& Collector ) override;
	virtual FString GetReferencerName() const override
	{
		return TEXT("FStudentManager");
	}

private:
	class UStudent* SafeStudent = nullptr;
	class UStudent* UnSafeStudent = nullptr;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "StudentManager.h"
#include "Student.h"

void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
	if ( SafeStudent->IsValidLowLevel() )
	{
		Collector.AddReferencedObject(SafeStudent);
	}
}

FGCObject의 경우 SafeStudent는 AddReferencedObject에서 설정해주고, UnSafeStudent는 설정 안함

GetReferencerName은 클래스 이름 넣어주면 됨

 

GC 동작 전

 가비지 콜렉터 동작 전에 애플리케이션을 꺼버리면 전부 멀쩡히 나옴

 

GC 동작 후

가비지 컬렉터가 동작한 후에는,

UPROPERTY가 설정된 것들의 경우엔 GC에 의해 회수당하지 않았고,

추가적으로 FGCObject 상속받아 구현까지 한 것은 회수당하지 않았음

나머지는 회수당함