「Android/CS基礎 - メモリ管理」シリーズのPart2です。
先に、Part1を読むことを推奨します。
また、本記事は私がAndroidのメモリ管理やGCについて学んだ内容をまとめたものです。誤りや改善点がありましたら、コメントでご指摘いただけると嬉しいです🙇♂️
この記事で理解できること
- GC Rootsについて
- 到達可能性について
- 参照の種類について
GC Rootsについて
概要
先に用語を定義します。
- オブジェクトグラフ → 上記画像の全体のグラフを指します(オブジェクト間の参照関係を示す)
- GC Roots → オブジェクトグラフの赤いノード(全ての参照の親)
まず、オブジェクト間の参照関係は上記画像のように有向グラフで表現されます。
なぜ有向グラフ?
それはノードが複数の親から参照される場合を表現するためです。
GC Rootsでは親子関係を下記のように定義しています。
親:任意のオブジェクトへの参照を持つオブジェクト
子:親から参照されているオブジェクト
例えば、下記のようなコードを考えてみましょう。
val child = MyObject()
val parent1 = mutableListOf(child)
val parent2 = mutableListOf(child)
この時、childはparent1/parent2と二つの親から参照されています。
これをグラフとして表現するために有向グラフが用いられています。
GC Rootsとは
GC Rootsとは、ランタイムがプログラム実行のために管理している、特別な参照の集合です。
具体的には以下のような参照がGC Rootsになります:
- スタック上のローカル変数: 実行中のメソッドで使用している参照
- 静的変数(static変数): プログラム全体で共有される参照
- JNI参照: ネイティブコードから参照されているオブジェクト
- アクティブなスレッド: 実行中のスレッドオブジェクト
これらはプログラムが動作するために必須の参照であり、
ガベージコレクタはこれらを起点として
「どのオブジェクトがまだ必要か」を判定します。
つまり:
- 主目的: プログラム実行のための参照管理
- 副次的用途: GCの生存判定の起点
オブジェクトグラフとは
オブジェクトグラフとは、プログラム実行中のオブジェクト間の参照関係全体を表す有向グラフです。
GC Rootsとオブジェクトグラフの関係
オブジェクトグラフは1つですが、GC Rootsは複数の起点を持ちます:
[オブジェクトグラフ全体]
│
├─ [起点1(GC Root1): スタック領域]
│ └─ ローカル変数 → オブジェクトA → オブジェクトB
│
├─ [起点2(GC Root2): メソッドエリア]
│ └─ static変数 → オブジェクトC
│
└─ [起点3(GC Root3): Native Stack]
└─ JNI参照 → オブジェクトD
ガベージコレクタは、これらのすべての起点から辿って到達可能なオブジェクトを「生存中」と判定します。
到達可能性(Rechability)について
GC Rootsを起点として、参照を辿ってそのオブジェクトに到達できるかどうかを表します。
- 到達可能: GC Rootsから参照を辿って到達できる → 生存
- 到達不可能: GC Rootsからどう辿っても到達できない → 回収対象
走査アルゴリズム
これは非常にシンプルでBFS/DFSです。
AndroidだとDFSらしいです。(確証はありません)
ただ、ここは本質ではないです。
各ノードが到達可能かどうかを調べることが目的でありBFS/DFSのパフォーマンス(空間計算量及び時間計算量)は然程変わりませんからね。
到達可能性を判断するアルゴリズム
Mark & Sweepアルゴリズムが使われています。
名前の通り、MarkというフェーズとSweepというフェーズで分かれています。
Mark処理
GC Rootから各ノードを走査していき、到達したノードに対して各ノードが持つmarkbitというのを1にします。
このmarkbitというのが到達可能性を示すものです。
markbit=1 → 到達可能 = 別のオブジェクトから参照されている
markbit=0 → 到達不能 = 誰からも参照されていない
到達可能ノードをマーキングするという名前通りの挙動です。
Sweep処理
英語の意味通り、掃き集めるみたいな意味です。
ここでは二つの処理をしています。
1.不要ノードの回収(GC)
オブジェクトグラフ内におけるmarkbitが0のものを回収します。
厳密にはGCはヒープ内で実行されるので少し違いますが省略します。
2.markbitのリセット
全ノードのmarkbitをリセットします。
こうすることで、次回のGCが走った時に新たに到達不能ノードになったノードを把握することができます。
参照の種類について
参照と言っても種類分けすると4種類に分かれます。
強参照(Strong Reference)
val obj = MyObject()
普段使っているものですね。
特徴は
- GC Rootsから到達可能な限り絶対に回収されない
- 明示的にnullにしないと残る
弱参照(Weak Reference)
val weakRef = WeakReference(MyObject())
val obj = weakRef.get() // 次のGC後はnull
特徴は、
- 強参照と合わせて使われたりすることが多い
- 次のGCで確定で回収される
つまり、弱参照だけでは即座に回収されるので使い物にならないです。
ですから、強参照されているオブジェクトに対して弱参照をかけることで、
強参照が消えたら連動して消える、を再現出来ます。
そのため、わざわざnullを明示的にセットしなくていいのです。
ソフト参照(Soft Reference)
val softRef = SoftReference(MyObject())
val obj = softRef.get() // null の可能性あり
特徴は、
- メモリが逼迫すると回収される
- キャッシュなど一時的なものに有効
ファントム参照(Phantom Reference)
これはほとんど使わないので省略
まとめ
本記事では以下を理解しました:
GC Rootsについて
- オブジェクト間の参照関係はオブジェクトグラフ(有向グラフ)で表現される
- GC Rootsはプログラム実行に必須の参照(Stack上の変数、Method Areaのstatic変数など)
- Heapはオブジェクトの置き場であり、GC Rootsにはならない
到達可能性について
- GC Rootsからオブジェクトに到達できるかで生存を判定
- Mark & Sweepアルゴリズムで到達可能性を判定し、不要なオブジェクトを回収
- 到達可能 = 回収しない、到達不可能 = 回収する
参照の種類について
- Strong Reference(強参照): 通常の参照、明示的にnullにしない限り回収されない
- Weak Reference(弱参照): 強参照が消えると次のGCで回収される
- Soft Reference(ソフト参照): メモリ逼迫時に回収される
- Phantom Reference(ファントム参照): 特殊用途
以上をここで知ってもらえればと思います!!
次回(Part3)では、ARTとGCの関連性について詳しく解説します。
具体的にはAndroidのメモリ管理の実装詳細や、実践的なメモリリーク対策などを扱います。
