スポンサーリンク
最適化・パフォーマンス

Unityでメモリリークを防ぐ!C#のDisposeパターンとイベント管理の極意

最適化・パフォーマンス

Unityでゲームを作っていると、こんな経験はありませんか?

  • 最初は快適だったのに、長時間プレイするとだんだん重くなる
  • シーン遷移を何度か繰り返すと、動作が不安定になる
  • Profilerを見ると、メモリ使用量がなかなか減らない

もし心当たりがあるなら、それはUnity特有のメモリリークが静かに進行しているサインかもしれません。

やっかいなのは、こうしたコードが「今は普通に動いてしまう」ことです。 エラーも出ないし、テストプレイでも問題なさそう。 でもその裏で、不要になったオブジェクトや参照が少しずつ溜まり続け、ある日突然パフォーマンス劣化やクラッシュとして表に出てきます。

UnityはC#を使っているため、ガベージコレクション(GC)が自動でメモリを管理してくれます。 ただし、GCは万能ではありません。 特にUnityでは、

  • event / Action を解除し忘れる
  • static やシングルトンに参照を持たせたままにする
  • DisposeすべきリソースをGC任せにする

といった「設計上の油断」が、そのままメモリリークにつながりやすい環境です。

この記事では、単なるC#理論の解説では終わらせず、
Unityプロジェクトで実際に事故りやすいパターンにフォーカスして、

  • Unityで起こりやすいメモリリークの正体
  • IDisposable / DisposeパターンをUnity文脈でどう使うべきか
  • event / Action / UnityEventが原因になるリークの仕組み
  • GCに任せてはいけないケースの見極め方

を、できるだけ噛み砕いて解説していきます。

「今は動いているけど、このままで本当に大丈夫なのか?」 そんな不安を感じている方が、安全な設計へ一歩踏み出すための記事です。


  1. 結論:Unityのメモリリークは「参照管理」と「解放タイミング」で防げる
  2. Unityで起こりやすいメモリリークの正体
    1. GCは「参照が完全に切れたオブジェクト」しか回収しない
    2. Unity特有でリークが起きやすい典型パターン
    3. 「GCがあるから大丈夫」はUnityでは通用しない
  3. DisposeパターンをUnityでどう使うべきか
    1. IDisposableは「特別なケース」だけのものではない
    2. Unity文脈でのDisposeパターンの考え方
    3. MonoBehaviourではいつDisposeすべきか
    4. 「なんとなくDispose」が危険な理由
  4. event / Action / UnityEvent が引き起こすメモリリーク
    1. イベント登録は「参照を握られる」こと
    2. Unityで特に危険なイベントパターン
    3. ラムダ式登録が危険になりやすい理由
    4. Unityでの正しいイベント管理テンプレート
  5. GCに任せてはいけないケースの見極め
    1. GCに任せても問題になりにくいケース
    2. GCに任せると危険になりやすいケース
    3. Editor設定がリークを見えにくくすることもある
  6. 「動くけど危険」なコードを安全設計に変える指針
    1. 参照の「所有者」をはっきりさせる
    2. Unityのライフサイクルに責務を割り当てる
    3. 「とりあえずstatic」は避ける
  7. よくある誤解・事故パターン
    1. Destroyすればメモリは必ず解放される
    2. eventは軽いから解除しなくても大丈夫
    3. GCが勝手に全部やってくれる
    4. メモリリークは特殊なバグ
  8. まとめ
  9. 参考文献・公式ドキュメント
  10. よくある質問(FAQ)
    1. 関連記事:

結論:Unityのメモリリークは「参照管理」と「解放タイミング」で防げる

先に結論からお伝えします。

Unityで発生するメモリリークの多くは、難しい最適化不足GCの性能問題が原因ではありません。 ほとんどの場合、

  • 不要になったオブジェクトへの参照を切っていない
  • 解放が必要なリソースの破棄タイミングを設計していない

この2つが根本原因です。

特にUnityでは、

  • event / Action を登録したまま解除していない
  • static やシングルトンが意図せず参照を保持している
  • Disposeすべきリソースを「GCが何とかしてくれる」と思っている

といったコードが、「動くけど危険」な状態を作りがちです。

ここで重要なのは、Disposeやイベント解除はパフォーマンスチューニングの話ではないという点です。 これは最適化ではなく、安全設計・事故防止の領域になります。

また、GCは「不要になったら即解放してくれる魔法」ではありません。 GCはあくまで参照が完全に切れたオブジェクトしか回収できず、 Unityのライフサイクル(シーン遷移・DontDestroyOnLoad・Editor設定)とは噛み合わない場面も多く存在します。

つまり、

  • GCに任せていいケース
  • 自分で責任を持って解放すべきケース

見極められるかどうかが、メモリリークを防げるかどうかの分かれ道です。

この先では、Unityで特に踏みやすいメモリリークの正体を整理しながら、 「どこまでやれば安全なのか」「何をやらなくてもいいのか」を実例ベースで解説していきます。




Unityで起こりやすいメモリリークの正体

まず押さえておきたいのは、Unityのメモリリークは「メモリが解放されないバグ」というより、 「もう使っていないのに、参照が残り続けている状態」だという点です。

UnityはC#で動いているため、ガベージコレクション(GC)が不要なオブジェクトを自動で回収してくれます。 ただしGCは、ある条件を満たしたオブジェクトしか回収できません

GCは「参照が完全に切れたオブジェクト」しか回収しない

GCは、いわゆる「ルート参照」から辿れるオブジェクトを「まだ使われている」と判断します。

代表的なルート参照は次のようなものです。

  • static フィールド
  • 実行中のスレッドが保持している参照
  • イベント(event / Action)に登録された参照

これらのどこかに参照が1本でも残っている限り、 見た目上は不要でも、GCは絶対に回収しません

そのため、

  • GameObjectをDestroyした
  • シーンを切り替えた
  • 画面から見えなくなった

といった状態でも、参照が残っていればメモリ上では生き続けます。

Unity特有でリークが起きやすい典型パターン

Unityでは、次のような構成が特にメモリリークの温床になりやすいです。

  • static変数やシングルトンがシーン内オブジェクトを参照している
  • DontDestroyOnLoadを使ったオブジェクトがイベントを抱えたまま
  • イベント登録だけして解除を忘れている
  • ネイティブリソースやIDisposableをGC任せにしている

これらはどれもエラーが出ないまま静かに進行します。 だからこそ、「長時間プレイすると重くなる」「何回か遊ぶと落ちる」といった形で後から表面化します。

「GCがあるから大丈夫」はUnityでは通用しない

GCは確かに便利ですが、Unityの実行モデルと完全に噛み合っているわけではありません

特にシーン遷移やEditor設定(Domain Reload無効化)などが絡むと、

  • 想定より長くオブジェクトが生存する
  • staticな参照がリセットされない

といった事態が普通に起こります。

GCの仕組みや「どこまで任せてよくて、どこから自分で管理すべきか」を もう一段深く知りたい場合は、次の記事も参考になります。

次は、こうしたリークを防ぐために欠かせない DisposeパターンをUnityでどう使うべきかを見ていきましょう。




DisposeパターンをUnityでどう使うべきか

メモリリーク対策の話になると、よく出てくるのがIDisposable / Disposeパターンです。 ただ、Unity開発では「正直あまり使ったことがない」という人も多いと思います。

でも実は、UnityはDisposeを意識しないと事故りやすい環境でもあります。

IDisposableは「特別なケース」だけのものではない

Disposeが必要になるのは、何も低レベルなC#ライブラリだけではありません。

Unityプロジェクトでも、次のようなケースではDisposeが関係してきます。

  • ネイティブリソースを内部的に扱うクラス
  • 大きなメモリバッファや長寿命データを保持するクラス
  • 他のIDisposableオブジェクトを所有しているクラス

これらを「GCがそのうち回収してくれるだろう」と放置すると、 回収タイミングが制御できず、メモリ使用量が膨らみ続ける原因になります。

Unity文脈でのDisposeパターンの考え方

UnityでDisposeパターンを使うときに大切なのは、 完璧なC#理論を再現することではありません

重要なのは次の3点です。

  • このクラスは破棄が必要なリソースを持っているか
  • 誰がそのリソースの所有者なのか
  • いつ確実に解放されるべきか

特に「自分が生成したDisposableなオブジェクトを、最後まで面倒を見る」という 所有権の意識が欠けると、Dispose漏れが起こりやすくなります。

MonoBehaviourではいつDisposeすべきか

MonoBehaviourを使っている場合、Disposeを呼ぶタイミングで迷う人が多いです。

基本的な指針はとてもシンプルで、

  • そのオブジェクトが完全に不要になるタイミング

でDisposeを呼びます。

多くの場合、それはOnDestroyになります。 シーン遷移やDestroyによってMonoBehaviourが破棄されるときに、

  • イベント解除
  • 保持しているIDisposableの破棄

をまとめて行う設計が安全です。

逆に、OnDisableは「一時的に使われなくなる」だけのケースも多いため、 ライフサイクルの責務を分けて考えることが重要になります。

「なんとなくDispose」が危険な理由

Disposeは魔法の呪文ではありません。 適当に呼ぶと、逆にバグの原因になることもあります。

  • まだ使われているオブジェクトをDisposeしてしまう
  • 二重Disposeで例外を起こす
  • 責務の所在が分からなくなる

だからこそ、Unityでは

「このクラスは何を所有し、いつ責任を終えるのか」

を明確にした上でDisposeを使うことが大切です。

C#とUnityのライフサイクル設計を体系的に理解したい場合は、 以下のような書籍で一度整理しておくのもおすすめです。

Unityゲーム プログラミング・バイブル 2nd Generation
✅ Amazonでチェックする✅ 楽天でチェックする

次は、Disposeと並んでUnityのメモリリーク原因になりやすい「イベント管理」について見ていきましょう。




event / Action / UnityEvent が引き起こすメモリリーク

Unityのメモリリークで、最も踏まれやすく、かつ気づきにくい原因がイベント管理です。

イベントはとても便利ですが、仕組みを正しく理解していないと 「見えない参照」を大量に作り出してしまいます。

イベント登録は「参照を握られる」こと

C#の event / Action は、

  • イベントを発行する側(Publisher)
  • イベントを購読する側(Subscriber)

という関係で成り立っています。

重要なのは、イベントに登録した瞬間、PublisherがSubscriberへの参照を保持するという点です。

つまり、

  • Subscriberがもう不要になった
  • GameObjectをDestroyした

としても、イベント解除をしなければ Publisher側が参照を持ち続けるため、GCは回収できません。

Unityで特に危険なイベントパターン

Unityでは、次のような組み合わせが特に危険です。

  • staticイベントにMonoBehaviourを登録している
  • シングルトンがイベントを持っている
  • DontDestroyOnLoadなオブジェクトがPublisherになっている

これらはすべて、Publisherの寿命が非常に長いという共通点があります。

寿命の長いオブジェクトがイベントを持ち、 寿命の短いMonoBehaviourが購読すると、 解除しない限り一生参照が残る構造になります。

ラムダ式登録が危険になりやすい理由

次のような書き方をしたことはありませんか?

someEvent += () => DoSomething();

この書き方は一見スマートですが、解除が非常に困難です。

同じラムダ式を再現できないため、

  • -= で解除できない
  • 結果としてイベントが残り続ける

という事故につながりやすくなります。

Unityでは、イベント登録はメソッド参照を基本にし、 登録と解除を必ずセットで考えるのが安全です。

Unityでの正しいイベント管理テンプレート

もっとも事故が少ないのは、次のパターンです。

  • OnEnable でイベント登録
  • OnDisable または OnDestroy でイベント解除

これにより、

  • オブジェクトが無効化されたとき
  • 破棄されたとき

のどちらでも、参照が残らない設計になります。

event / Action の基本構造や、安全な実装パターンをもう少し詳しく知りたい場合は、 以下の記事も参考になります。

次は、「GCに任せてはいけないケース」を整理しながら、 どこまで自分で管理すべきかを見極めていきましょう。




GCに任せてはいけないケースの見極め

ここまでで、「参照が残っているとGCは回収できない」という話をしてきました。 では逆に、どんなときにGC任せでは危険になるのかを整理しておきましょう。

判断基準は意外とシンプルで、 「解放タイミングを自分で制御したいかどうか」です。

GCに任せても問題になりにくいケース

次のようなオブジェクトは、基本的にGC任せでも大きな問題になりにくいです。

  • 短命な一時オブジェクト
  • Update内で一時的に生成される計算用データ
  • 参照関係が単純で寿命が明確なオブジェクト

これらは、多少回収が遅れても致命的な影響が出にくく、 Unityでも一般的な使い方になります。

GCに任せると危険になりやすいケース

一方で、次のようなケースではGC任せはおすすめできません。

  • ネイティブリソース(ファイル、ハンドル、バッファなど)
  • 大きなメモリを占有するオブジェクト
  • 寿命が長いオブジェクトが保持している参照
  • static フィールドやシングルトン経由で参照されているもの

特にstatic / シングルトンは、 アプリケーション終了まで生き続ける「強力なルート参照」になります。

そのため、

  • シーン内オブジェクトをstaticで保持している
  • シングルトンがイベント購読者を抱えたまま

といった構成は、非常にリークを起こしやすいです。

シングルトン設計そのものが悪いわけではありませんが、 使い方を間違えるとメモリリークの温床になります。

シングルトンでやりがちなミスや、安全な実装パターンについては、 次の記事で詳しく解説しています。

Editor設定がリークを見えにくくすることもある

Unity Editorでは、「Domain Reload」を無効にしていると、

  • static変数の値
  • イベントの購読状態

がPlayモード終了後も残ります。

これは開発効率を上げる一方で、 本来なら初期化されるはずの参照が残るという落とし穴でもあります。

そのため、

  • 明示的な初期化処理を書く
  • Playモード開始時にリセットする設計にする

といった対策が必要になります。

次は、「動くけど危険」なコードを、 どうすれば安全な設計に変えられるのかを整理していきましょう。




「動くけど危険」なコードを安全設計に変える指針

ここまでで、メモリリークの原因や危険なパターンはかなり見えてきたと思います。 では最後に、「今は動いているけど不安なコード」を どうやって事故らない設計に変えていくかを整理しましょう。

参照の「所有者」をはっきりさせる

安全設計の第一歩は、とてもシンプルです。

「このオブジェクトは、誰が作って、誰が最後まで責任を持つのか」

を明確にすることです。

特に注意したいのが、IDisposableを持つクラスやイベントを登録するクラスです。

  • 自分が生成したIDisposableは、自分でDisposeする
  • 自分が登録したイベントは、自分で解除する

このルールを守るだけで、リーク事故はかなり減らせます。

Unityのライフサイクルに責務を割り当てる

Unityでは、すべてをOnDestroyに詰め込みがちですが、 それが逆に分かりにくさやバグを生むこともあります。

おすすめなのは、ライフサイクルごとに役割を分けることです。

  • Awake:依存関係の取得・初期化
  • OnEnable:イベント登録・処理開始
  • OnDisable:イベント解除・一時停止
  • OnDestroy:最終的な破棄・Dispose

こうしておくと、「どこで何をしているスクリプトなのか」が 自分にも他人にも分かりやすくなります。

「とりあえずstatic」は避ける

staticはとても便利ですが、 強力すぎる参照を作る点を忘れてはいけません。

「一時的にアクセスしやすいから」という理由だけでstaticにすると、

  • いつ解放されるのか分からない
  • どこから参照されているのか追えない

という状態になりがちです。

staticを使う場合は、

  • 寿命がアプリ全体であることを許容できるか
  • シーン内オブジェクトを参照していないか

を必ず確認しましょう。

設計全体を整理したい場合は、 SOLID原則の考え方がとても役に立ちます。

次は、初心者が特に勘違いしやすい 「よくある誤解と事故パターン」をまとめて整理します。




よくある誤解・事故パターン

ここまで理解していても、Unityのメモリ管理まわりは 「なんとなくの思い込み」で事故を起こしやすい分野です。

ここでは、実際によくある誤解と、それがなぜ危険なのかを整理します。

Destroyすればメモリは必ず解放される

これは非常によくある誤解です。

GameObjectをDestroyすると、Unityのオブジェクトとしては破棄されますが、 C#オブジェクトの参照が残っていれば、GCは回収できません。

  • static変数に参照が残っている
  • イベントに登録されたまま

といった場合、Destroyしてもメモリ上では生き続けます。

eventは軽いから解除しなくても大丈夫

event自体は軽量ですが、参照は軽くありません

解除しないイベントは、

  • 不要なオブジェクトを生かし続ける
  • 想定外のタイミングで処理が呼ばれる

といった二重の事故を引き起こします。

「一度しか呼ばれないから大丈夫」 「小規模だから問題ない」

こうした判断が、後から一番つらい形で返ってきます。

GCが勝手に全部やってくれる

GCは非常に優秀ですが、何を解放すべきかは教えてくれません

参照が残っていれば回収できませんし、 回収タイミングもUnityの都合とは一致しません。

そのため、

  • GCがある=メモリ管理不要

という考え方は、Unityでは特に危険です。

メモリリークは特殊なバグ

実際には、メモリリークは設計ミスの積み重ねで起こります。

しかも、

  • すぐには問題が出ない
  • テストでは気づきにくい

という特徴があるため、 「気づいたときには手遅れ」になりやすいのです。




まとめ

Unityのメモリリークは、難解なバグや特殊な最適化不足が原因で起こるものではありません。 多くの場合、参照管理と解放タイミングを設計していないことが原因です。

この記事でお伝えしてきたポイントを、改めて整理します。

  • GCは「参照が完全に切れたオブジェクト」しか回収できない
  • event / Action の解除忘れは、見えない参照を生み出す
  • Disposeは最適化ではなく安全設計
  • static / シングルトンは強力な参照になりやすい
  • Unityのライフサイクルに責務を割り当てることが重要

特に大切なのは、 「今は動いている」コードを疑う視点です。

エラーが出ない、テストで問題がない、という理由だけで安心せず、

  • この参照はいつ切れるのか
  • このリソースは誰が最後に解放するのか

を一度立ち止まって考えるだけで、 後から起こるトラブルの多くを防げます。

私自身も、メモリリークで「原因が分からず何日も悩む」経験を何度もしてきました。 だからこそ、この記事が同じ事故を避けるための地図になれば嬉しいです。

最後に、Unityのメモリ管理は一度理解すれば終わりではありません。 プロジェクトが大きくなるほど、設計の差がパフォーマンスや安定性として表れてきます。

「動く」だけでなく、 「長く安全に動き続けるUnityプロジェクト」を目指していきましょう 🙂


参考文献・公式ドキュメント

本記事の内容は、以下のUnity公式ドキュメント/Microsoft公式資料/技術解説記事を根拠として構成しています。 より深く理解したい場合は、あわせて参照してみてください。


よくある質問(FAQ)

Q
イベント解除は OnDestroy だけで本当に十分ですか?
A

ケースによりますが、OnDestroyだけでは不十分なことも多いです。

OnDestroyは「オブジェクトが完全に破棄されるとき」にしか呼ばれません。 そのため、

  • SetActive(false) で一時的に無効化される
  • シーン内で有効・無効を頻繁に切り替える

といった場合、イベントが登録されたままになり、 想定外のタイミングで処理が呼ばれることがあります。

安全なのは、

  • OnEnable でイベント登録
  • OnDisable でイベント解除

というペア設計です。 OnDestroyは「最終保険」として使う、という意識がおすすめです。

Q
小規模なゲームや個人開発でもDisposeは必要ですか?
A

必要になるケースはあります。

特に、

  • 長時間プレイを想定している
  • シーン遷移を何度も行う
  • 大きなデータや外部リソースを扱っている

場合は、規模に関係なくDispose設計の差が出ます。

逆に、すべてのクラスで無理にDisposeを実装する必要はありません。 「解放タイミングを自分で制御したいか」を基準に判断すると迷いにくいです。

Q
Profilerではどこを見ればメモリリークに気づけますか?
A

まず注目したいのは、次のポイントです。

  • GC Used Memory が徐々に増え続けていないか
  • シーン遷移後も Object Count が減っていないか

一度の増減ではなく、「操作を繰り返すたびに増えて戻らない」挙動が見えたら要注意です。

また、Memory Profilerを使うと、

  • どの型のオブジェクトが増えているか
  • どこから参照されているか

まで追えるため、 「原因が分からないメモリ増加」に直面したときの強い味方になります。

慣れないうちは、 「シーン遷移前後でメモリが戻っているか」 これだけでも意識すると、リークに早く気づけるようになります。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク