【UE】UE のガベージコレクションについて

Unreal Engine

はじめに

ガベージコレクションとは

プログラム実行時、「NewObject()」や「UWorld::SpawnActor()」等で生成した UObject はメモリに格納される。
(メモリ領域の確保)

しかし、生成はされたものの初めから一切使用されなかったり、途中から使用されなくなった UObject がメモリに残っている事がある。
(「使用されない」というのは正確には参照が無くなり、孤立した UObject の事)

その UObject の分だけ使えるメモリ領域が減ってしまっているため、その UObject は邪魔である。
(メモリリーク)

そこで、使用されていない(参照関係の無い)UObject を自動で破棄(メモリを解放)してくれる機能が UE におけるガベージコレクションであり、ガベージコレクションを行うのがガベージコレクタである。

ガベージコレクタによるガベージコレクトは基本的にエンジンが適切なタイミングで自動的に実行するが、「UEngine::ForceGarbageCollection()」を呼び出す等する事によって任意のタイミングで実行する事も出来る。

意図的に UObject を破棄するには

基本的にガベージコレクションを用いる事なく delete 等によってプログラマーが意図的に UObject を即座に破棄する事は出来ない。

プログラマーが意図的に UObject を破棄するには「UObjectBaseUtility::MarkPendingKill()」を呼び出して UObject をガベージコレクションの回収対象としてマークする必要がある。

しかし、直接「UObjectBaseUtility::MarkPendingKill()」を呼び出す事はほとんど無く、AActor クラスなら「AActor::Destroy()」、UActorComponent クラスなら「UActorComponent::DestroyComponent()」を呼び出す事で間接的にガベージコレクションの回収対象にしている。

「UPROPERTY()」の役割

ダングリングポインタを防ぐ

UObject がガベージコレクションによってメモリから解放されても UObject* 型の変数の宣言時に「UPROPERTY()」を付けないと、その変数は nullptr にならずに別のオブジェクトのメモリ領域を指し示し続け、そこにアクセスされる可能性がある。

これをダングリングポインタといい、「UObjectBase::IsValidLowLevel()」を使用する事でそのポインタがダングリングポインタの可能性があるかどうかを判断する事が出来るが、そのポインタの指し示すメモリ領域に別の UObject が格納される等した場合は正確に判断できないため、「UObjectBase::IsValidLowLevel()」は確実にダングリングポインタかどうかを判断できる訳ではない。

UObject* 型の変数の宣言時に「UPROPERTY()」を付けると、その UObject がガベージコレクションによって破棄された時にその変数に自動で nullptr を代入してくれるため、その変数の指し示していたメモリ領域にアクセスされる事を事前に防ぐ事が出来る。
(ダングリングポインタの防止)

参照関係をガベージコレクタに伝える

UObject* 型の変数を宣言する際に「UPROPERTY()」を付けないとその UObject がどこからも参照されていない事になり、ガベージコレクトされた時にその UObject も破棄されてしまう。

この時、UObject は PendingKill の状態(メモリ上には存在しているが、ガベージコレクションの回収対象としてマークされている状態)を経由する事なくメモリから解放される。

しかし、UObject* 型の変数を宣言する際に「UPROPERTY()」を付けると「その UObject は参照されている」という情報をガベージコレクタに伝える事が出来るため、ガベージコレクタによる意図しないオブジェクトの破棄を事前に防ぐ事ができ、その変数が宣言されていたクラスが破棄される等してその UObject がどこからも参照されなくなると、その UObject はガベージコレクションによって破棄される。

IsValid()

UObject に安全にアクセスするためにはその UObject のポインタが nullptr ではない事と PendingKill の状態では無い事の2つを同時に確認する必要がある。

「IsValid(UObject*)」ではそれらを一度に判断できるため、基本的に UObject の確認には「IsValid()」を用いると良い。

void ASampleActor::Hoge(UObject* Object)
{
    if(IsValid(Object))
    {
        // Object が有効だった場合の処理
    }
}

「UPROPERTY()」の活用例

ダングリングポインタを防ぐ

以下のコードでは AActor* 型の ActorPtr 変数が ASampleActor クラスのメンバ変数として宣言されているが、「UPROPERTY()」が付いていない。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    TObjectPtr<AActor> ActorPtr;//「UPROPERTY()」が無い
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

このクラスでは「BeginPlay()」で AActor クラスを生成し、その生成したオブジェクトのポインタを ActorPtr 変数に代入後、すぐにそのオブジェクトを「Destroy()」して PendingKill の状態にしている。

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASampleActor::BeginPlay()
{
    Super::BeginPlay();

    ActorPtr = GetWorld()->SpawnActor<AActor>();

    ActorPtr->Destroy();
}

void ASampleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (IsValid(ActorPtr))
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid"));
    }

    if (ActorPtr == nullptr)
    {
        UKismetSystemLibrary::PrintString(this, TEXT("nullptr"));

        return;
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not nullptr"));
    }

    if (ActorPtr->IsPendingKillPending())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("pending kill"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not pending kill"));
    }

    if (ActorPtr->IsValidLowLevel())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid low level"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid low level"));
    }
}

その後は「Tick()」で毎フレーム、ActorPtr 変数の状態を確認しており、ガベージコレクト前は「not valid」「not nullptr」「pending kill」「valid low level」と表示され、

ガベージコレクト前(UPROPERTY 無し)

ガベージコレクト後は「not valid」「not nullptr」「pending kill」「not valid low level」と表示される。

ガベージコレクト後(UPROPERTY 無し)

つまり、ActorPtr 変数の指し示していたオブジェクトは破棄されているにも関わらず、ActorPtr 変数には nullptr が代入されていないため、ActorPtr 変数は本来の目的のオブジェクトを指し示すポインタではなくなり、別のオブジェクトのメモリ領域を指し示す無効なポインタになってしまっている。
(ダングリングポインタ)

しかし、以下のコードのように ActorPtr 変数の宣言時に「UPROPERTY()」を付けると

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    UPROPERTY() //「UPROPERTY()」を追加
    TObjectPtr<AActor> ActorPtr;
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

ガベージコレクト前は先程と同様に「not valid」「not nullptr」「pending kill」「valid low level」と表示されるが、

ガベージコレクト前(UPROPERTY あり)

ガベージコレクト時に ActorPtr 変数に nullptr が自動的に代入され、ガベージコレクト後は「not valid」「nullptr」と表示される。

ガベージコレクト後(UPROPERTY あり)

つまり、ActorPtr 変数が別のオブジェクトのメモリ領域を指し示す事が無くなり、ダングリングポインタを事前に防いでくれるようになっている。

ガベージコレクションによって破棄されないようにする

以下のコードでは UObject* 型の ObjectPtr 変数が ASampleActor クラスのメンバ変数として宣言されているが、「UPROPERTY()」が付いていない。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    TObjectPtr<UObject> ObjectPtr;//「UPROPERTY()」が無い
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

このクラスでは「BeginPlay()」で UObject クラスを生成し、その生成したオブジェクトのポインタを ObjectPtr 変数に代入後、「Tick()」で毎フレーム、ObjectPtr 変数の状態を確認している。

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASampleActor::BeginPlay()
{
    Super::BeginPlay();

    ObjectPtr = NewObject<UObject>();
}

void ASampleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
	
    if (IsValid(ObjectPtr))
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid"));
    }

    if (ObjectPtr == nullptr)
    {
        UKismetSystemLibrary::PrintString(this, TEXT("nullptr"));

        return;
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not nullptr"));
    }

    if (ObjectPtr->IsValidLowLevel())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid low level"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid low level"));
    }
}

ガベージコレクト前は「valid」「not nullptr」「valid low level」と表示され、有効な状態だが、

ガベージコレクト前(UPROPERTY 無し)

ガベージコレクト後は「valid」「not nullptr」「not valid low level」と表示される。

ガベージコレクト後(UPROPERTY 無し)

つまり、「ObjectPtr 変数の指し示しているオブジェクトは誰かから参照されている」という情報がガベージコレクタに伝わらず、ObjectPtr 変数の指し示しているオブジェクトがガベージコレクト時に破棄されてしまっている。

そして、ObjectPtr 変数の指し示していたオブジェクトは破棄されているにも関わらず、ObjectPtr 変数には nullptr が代入されていないため、ObjectPtr 変数は本来の目的のオブジェクトを指し示すポインタではなくなり、別のオブジェクトのメモリ領域を指し示す無効なポインタになってしまっている。
(ダングリングポインタ)

しかし、以下のコードのように ObjectPtr 変数の宣言時に「UPROPERTY()」を付けると

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    UPROPERTY() //「UPROPERTY()」を追加
    TObjectPtr<UObject> ObjectPtr;
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

「ObjectPtr 変数の指し示しているオブジェクトは誰かから参照されている」という情報がガベージコレクタに伝わるため、ガベージコレクト後も「valid」「not nullptr」「valid low level」と表示され、ObjectPtr 変数の指し示しているオブジェクトがガベージコレクタによって破棄されないようになる。

ガベージコレクト前後(UPROPERTY あり)

最後に

参考記事

お問い合わせ

    タイトルとURLをコピーしました