Help us understand the problem. What is going on with this article?

Unity コンポーネントがDestroyされても、オブジェクトがGCで回収されないかもしれない話

More than 1 year has passed since last update.

はじめに

まずはこちらの Unityのnullはnullじゃないかもしれない を読んでから、この記事をお読みください。

起きる現象

「コンポーネントが別のコンポーネントを参照しており、その参照先のコンポーネントがDestroy()されたときにリークが発生する可能性がある。」

どういうこと?

下準備

例えば適当なクラスがあったとして

public class Apple
{
}

このオブジェクトをフィールドに持つコンポーネント(被参照側)があり

using UnityEngine;

public class DeathObject : MonoBehaviour
{
    /// <summary>
    /// Appleはこのコンポーネントに紐づく適当なオブジェクトのつもり
    /// </summary>
    private Apple Apple = new Apple();

    void Start()
    {
        Destroy(gameObject, 3.0f); //3秒後にGameObjectを削除
    }

    void OnDestroy()
    {
        Debug.Log("OnDestroy");
    }
}

このコンポーネントをフィールドに持つコンポーネントがあったとする。

using UnityEngine;

/// <summary>
/// おくりびと
/// </summary>
public class Okuribito : MonoBehaviour
{
    /// <summary>
    /// DeathObjectをフィールドに持つ
    /// </summary>
    [SerializeField] private DeathObject _deathObject;
}

挙動

DeathObjectがDestroy()されても、Appleのインスタンスが残り続けてしまう。

なぜ?

まず、クラスの参照関係はこうなります。

Okuribito → DeathObject → Apple

ここでDeathObjectがDestroyされるとどうなるかですが、DeathObject見かけ上はnullになります。

ですが、OkuribitoDeathObjectを参照したままなので、参照カウントは残っている状態になります。
つまりガベージコレクタ的にはDeathObjectはまだ生きている判定になります。
そのため、DeathObjectに対してアクセスするとnullを返すのに、実はインスタンスへの参照は残っているという状況になってしまいます。

Okuribito → DeathObject(Destroyはしたけどインスタンスは残ってる) → Apple

すなわち、Appleがリークした原因はDeathObjectもリークしているからです。
OkuribitoがDestroyされない限り、DeathObjectが解放されず、連動してAppleも解放されない、という状態になってしまいます。

これ問題になるの?

結局はOkuribitoがDestroyされたら、次のGCで全部まとめて回収されることになります。
なので実際はリークにはなりません。

ただ「次のGCで回収されるだろうな」と思っていたオブジェクトがシーン遷移のタイミングまでメモリに積まれぱなしだった、ということは起きるかも。

対処療法

気になるなら、OnDestroy()時にフィールドで持っているオブジェクトへの参照を外してあげればいいと思います。
対象へ参照している部分をnullで上書きしてあげればOKです。

ただ、null代入するよりかは、IDisposable.Dispose()を呼び出すとかそっちの方が重要だと思います。というかDispose()は使い終わったら必ず呼べ。

あとは、フィールドにIEnumerable<T>を持っていたりした場合は、そこに入ってるオブジェクトもずっと保持されたまま残ってしまいます。こういったものは明示的にnullを代入するなり、ListならClear()するなりした方が良いです。

using UnityEngine;

public class DeathObject : MonoBehaviour
{
    /// <summary>
    /// Appleはこのコンポーネントに紐づく適当なオブジェクトのつもり
    /// </summary>
    private Apple Apple = new Apple();

    void Start()
    {
        Destroy(gameObject, 3.0f); //3秒後にGameObjectを削除
    }

    void OnDestroy()
    {
        Apple = null; // Destroyのタイミングで参照を外す
    }
}

所感

Destroy()したからといって、リソースの解放がただちに完了するわけではないという点に注意しないとダメそう。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away