【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()」を付けないと、その UObject* 型の変数は nullptr にならないため、UObject* 型の変数が別のオブジェクトのメモリ領域を指し示していたとしても、そこにアクセスされる可能性がある。

これをダングリングポインタといい、「UObjectBase::IsValidLowLevel()」を使用する事でそのポインタがダングリングポインタの可能性があるかどうかを判断する事が出来る。

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

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

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

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

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

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

その UObject* 型の変数が宣言されていたクラスが破棄される等してその UObject がどこからも参照されなくなると、その UObject はガベージコレクションによって破棄される。

IsValid()

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

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

if(IsValid({ UObject* 型の値}))
{
    // UObject* が有効だった際の処理
}

「UPROPERTY()」の活用例

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

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

このクラスでは「BeginPlay()」で AActor クラスをスポーン(インスタンス化)してそのインスタンスのポインタを Actor 変数に代入後、すぐにその AActor を「Destroy()」して PendingKill の状態にしている。

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

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

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

private:
    AActor* Actor;

public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};
ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

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

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

    Actor->Destroy();
}

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

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

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

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

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

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

しかし、Actor 変数の宣言時に「UPROPERTY()」を付けるとガベージコレクト後に Actor 変数に nullptr が自動で代入され、ガベージコレクト後は「not valid」「nullptr」と表示される。

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

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

private:
    UPROPERTY()
    AActor* Actor;

public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

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

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

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

このクラスでは「BeginPlay()」で UObject クラスをインスタンス化してそのインスタンスのポインタを Object 変数に代入後、「Tick()」で毎フレーム、Object 変数の状態を確認している。

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

「UPROPERTY()」を付けないと「Object 変数の指し示している UObject は参照されている」という情報がガベージコレクタに伝わらず、ガベージコレクト時に Object 変数の指し示す UObject が破棄されてしまう。

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

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

private:
    UObject* Object;

public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};
ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

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

    Object = NewObject<UObject>();
}

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

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

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

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

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

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

しかし、Object 変数の宣言時に「UPROPERTY()」を付けると「Object 変数の指し示している UObject は参照されている」という情報がガベージコレクタに伝わるため、ガベージコレクトしても「valid」「not nullptr」「not pending kill」「valid low level」と表示され、Object 変数の指し示している UObject がガベージコレクタによって破棄されないようになる。

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

private:
    UPROPERTY()
    UObject* Object;

public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

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

最後に

参考記事

お問い合わせ

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