우리가 MyGame.exe를 실행하면, 운영체제는 이 프로그램을 위해 프로세스라는 독립된 실행 환경을 구축한다.
OS는 이 프로세스에게 가상 메모리를 할당하는데, 이 가상 메모리는 크게 4개의 영역으로 나뉜다.

우리가 작성한 C++ 코드가 컴파일되어 CPU가 직접 읽을 수 있는 기계어로 번역된 데이터가 저장된다.
프로그램 시작 시 로드되어, 프로그램이 종료될 때까지 유지된다.
Code 영역의 핵심 특징은 읽기 전용/실행 전용이라는 것이다.
CPU는 이곳의 명령어를 읽고 실행할 수만 있다.
만약 실행 중에 프로그램이 이 COde 영역의 내용을 수정하려 시도하면 OS는 이를 심각한 보안 위협으로 간주하고 프로세스를 즉시 강제 종료시킨다.
또한, 프로그램이 실행되는 도중에 이 영역의 크기나 내용이 변하지 않는다.
전역 변수와 정적 변수가 저장되는 영역이며, 프로세스의 수명과 동일하다.
Static 영역은 두 부분으로 나뉜다.
.exe 파일 자체에 이 초기값이 저장되어 있다가 프로그램 로드 시 메모리에 그대로 복사.exe 파일에는 '이만큼의 공간이 필요하다'는 정보만 기록 (파일 크기 절약)전역 변수는 모든 곳에서 접근 가능해 편리하지만, 데이터가 언제 어디서 변경되었는지 추적하기가 극도로 어려워져 디버깅이 힘들어진다.
Unreal에서는 원시 C++ 전역 변수 대신 UGameInstance나 UGameSingleton을 사용하도록 강력히 권장한다.
이 객체들은 힙(Heap)에 생성되며, 게임의 수명 내내 존재하고 전역적인 데이터 저장소 역할을 한다.
// 1. Data 영역 (초기값이 0이 아님)
int G_TotalScore = 100;
const char* G_GameName = "MyUnrealGame"; // (문자열 리터럴도 특수 영역에 저장됨)
// 2. BSS 영역 (초기값 없거나 0 -> OS가 0으로 채워줌)
bool G_IsGameRunning; // = 0 (false)
float G_DamageModifiers[100]; // 모든 요소가 0.0f로 초기화됨
void MyFunction()
{
// 이 변수는 스택이 아닌 Static 영역에 저장.
// 이 코드는 프로그램 실행 중 최초 1회만 실행.
static int FunctionCallCount = 0;
}
함수 호출 시 생성되는 지역 변수, 함수에 전달되는 매개 변수, 함수가 끝나고 돌아갈 복귀 주소가 저장된다.
함수가 시작될 때 생성되고, 함수가 return 할 때 즉시 사라진다.
main()이 A()를 호출하고, A()가 B()를 호출하면, 스택에는 main->A->B 순서로 스택 프레임이 쌓임스택은 빠르기 때문에 가능한 한 스택을 활용하는 것이 좋다.
하지만 FTransform, FHitResult, TArray 같은 큰 구조체나 컨테이너를 함수에 값으로 전달하면 모든 데이터가 스택에 통째로 복사된다.
따라서 매개변수를 참조형으로 선언하면 원본의 주소만 스택에 복사되므로 효율적이다.
프로그래머가 코드 실행 중에 동적으로 할당하는 모든 데이터이다.
프로그래머가 요청할 때 생성되며, 명시적으로 해제하거나 GC가 수거할 때까지 절대 자동으로 사라지지 않는다.
new로 빌린 메모리를 delete로 반납하는 것을 잊는 경우이다.
// ----------------------------------------
// 순수 C++의 '메모리 누수' 예제
// ----------------------------------------
class MyData { public: int Value = 100; };
void CreateData_Leak()
{
// 1. 'Ptr' 변수 자체는 *스택*에 생성
// 2. 'Ptr'이 가리키는 *실제 데이터*는 *힙*에 생성 (주소: 0x1000A000)
MyData* Ptr = new MyData();
// 3. 'delete Ptr;'을 안 함
}
// 4. 함수 리턴
// 5. *스택*에 있던 'Ptr' 변수는 사라짐
// 6. 결과:
// 아무도 'MyData' 객체(0x1000A000)의 주소를 모르므로,
// 이 메모리는 프로그램 종료 시까지 영원히 "누수"됨
메모리 누수보다 더 심각한 문제이다.
delete는 했지만, 다른 포인터가 여전히 그 삭제된 주소를 가리키고 있다가 접근할 때 발생한다.
MyData* Ptr_A = new MyData(); // A가 객체 생성 (0x1000A000)
MyData* Ptr_B = Ptr_A; // B가 A와 동일한 객체를 가리킴
delete Ptr_A; // A가 객체를 삭제함
Ptr_A = nullptr;
// Ptr_B는 Ptr_A가 객체를 삭제한 사실을 모릅니다.
// Ptr_B는 여전히 '삭제된' 주소 0x1000A000을 가리키고 있습니다. (Stale Pointer)
Ptr_B->Value = 300; // CRASH! (존재하지 않는 메모리에 접근)
Unreal Engine의 모든 UObject는 힙 영역에 생성된다.
Unreal은 위에서 언급된 메모리 누수와 Dangling Pointer 문제를 GC와 UPROPERTY라는 시스템으로 동시에 해결한다.
예를 들어 다음과 같은 코드가 있다고 가정해보자.
// (헤더 파일: AMyPlayer.h)
UCLASS()
class AMyPlayer : public APawn
{
GENERATED_BODY()
// GC에게 정보를 등록하는 키워드
UPROPERTY()
TObjectPtr<AMyActor> MyActorPtr; // (TObjectPtr은 UE5의 안전한 포인터)
};
// (소스 파일: AMyPlayer.cpp)
void AMyPlayer::SpawnMyActor()
{
// 2. 힙에 'AMyActor' 객체 생성 (주소: 0x2000B000)
AMyActor* MySpawnedActor = GetWorld()->SpawnActor<AMyActor>();
// 3. 'UPROPERTY' 멤버 변수에 주소(0x2000B000) 저장
this->MyActorPtr = MySpawnedActor;
}
UPROPERTY는 GC에게 '이 객체(AMyActor)는 AMyPlayer가 여전히 사용 중이니, '쓰레기가 아니다' 라고 알려주는 강한 참조 신호이다.
결과적으로 AMyPlayer 객체가 파괴되지 않는 한, GC는 MyActorPtr가 참조하는 AMyActor를 절대 쓰레기로 취급하지 않고 삭제하지 않는다.
즉, 개발자는 new만 하고 delete를 잊는 메모리 누수 걱정을 할 필요가 없다.
나중에 AMyPlayer가 파괴되거나 MyActor = nullptr이 되어 참조가 끊기면, GC가 알아서 AMyActor를 수거해간다.
UPROPERTY의 진짜 핵심이다.
UPROPERTY가 있다면 GC에게 '만약 이 객체(AMyActor)가 삭제되면, 나(MyActorPtr)에게 즉시 알려줘' 라고 등록하는 '사망 통지' 예약과 같다.
GC가 AMyActor를 삭제하기로 결정하는 순간, 엔진 GC 코드가 MyActorPtr 변수의 메모리에 직접 접근하여 값을 nullptr로 강제 override 해버린다.
결과적으로 개발자는 if (MyActorPtr) 또는 if (IsValid(MyActorPtr))이라는 간단한 검사만으로 삭제된 객체에 접근하려는 시도를 100% 막을 수 있다.
UPROPERTY 매크로를 발견하면, UHT가 'AMyPlayer 클래스는 XXX 바이트 위치에 AMyActor를 가리키는 포인터가 있다'는 '명단'을 생성하여 엔진에 등록AMyActor(0x2000B000)를 쓰레기로 판단하고 삭제하기로 결정0x2000B000 주소를 가리키는 UPROPERTY가 누구누구지?AMyPlayer 객체의 MyActorPtr 변수가 가리키고 있네MyActorPtr 변수의 메모리에 직접 접근하여 그 값을 nullptr로 강제 overrideAMyActor를 deleteGC의 관리를 받는 모든 객체는 UObject를 상속받아야 한다.
new 키워드가 아닌, NewObject<T>()(UObject용) 또는 GetWorld()->SpawnActor<T>()(AActor용)를 사용해야함GC는 '이 객체가 필요한가?'라는 어려운 질문에 대답하지 않는다.
대신 "이 객체에 '도달'할 수 있는가?"라는 간단한 질문에만 대답한다.
루트 셋은 '절대로 GC의 대상이 되어서는 안 되는, 항상 살아있다고 보장된' 객체들의 최상위 목록이다.
GC는 항상 이 루트 셋에서부터 탐색을 시작한다.
UGameInstance (게임이 켜져 있는 내내 존재)UWorld (현재 로드된 레벨)AddToRoot() 함수로 명시적으로 루트에 추가된 객체GC가 실행되면 (보통 일정 시간 간격으로) 다음 두 단계를 거친다.
UPROPERTY 포인터를 따라감UPROPERTY를 건너 만나는 모든 UObject를 '살아있음'이라고 표시UPROPERTY를 통해 도달할 수 있는 모든 객체를 '살아있음'으로 표시UPROPERTY 포인터를 찾아서, 그 값을 nullptr로 override (Null-Setting)