JVMにおけるオブジェクトの状態とガベージコレクション

Java仮想マシン(JVM)のヒープ領域には、ほぼすべてのオブジェクトインスタンスが格納されています。ガベージコレクタがメモリを回収する前に、どのオブジェクトがまだ「生存」しており、どのオブジェクトが「死亡」しているかを判断する必要があります。この判定には主に以下の2つの方法があります。

参照カウント方式

アルゴリズムの概念:各オブジェクトに参照カウンタを追加し、どこかからそのオブジェクトが参照されるたびにカウンタを1増加させ、参照が無効になるたびに1減少させます。最終的にカウンタが0になったオブジェクトは、使用される可能性がなく「死亡」したと見なされます。

しかし、JavaではこのアルゴリズムはGCに使用されていません。最も大きな理由は、オブジェクト間の相互循環参照の問題を解決することが困難なためです。以下のコード例をご覧ください。

1 public class CircularReferenceDemo {
2     
3     public Object reference = null;
4     
5     public void demonstrateIssue(){
6         CircularReferenceDemo instanceA = new CircularReferenceDemo();
7         CircularReferenceDemo instanceB = new CircularReferenceDemo();
8         instanceA.reference = instanceB;
9         instanceB.reference = instanceA;
10         instanceA = null;
11         instanceB = null;
12         
13         System.gc();
14     }
15 }

上記のコードでは、instanceAとinstanceBが相互に参照し合っています。これら以外に参照は存在しませんが、相互に参照し合っているため参照カウンタが0にならず、GCコレクタがこれらを回収できない状況が発生します。

ルート検索アルゴリズム

Javaでは、ルート検索アルゴリズム(GC Roots Tracing)を使用してオブジェクトの生存を判断しています。

アルゴリズムの概念:「GC Roots」と呼ばれる特定のオブジェクトを起点として、これらのノードから下向きに検索を行います。検索で通過した経路を参照チェーン(Reference Chain)と呼びます。オブジェクトからGC Rootsへの参照チェーンが存在しない場合(到達不可能)、そのオブジェクトは使用不可と見なされます。

例えば、Object3とObject4が互いに接続されていても、GC Rootsへの経路がなければ、Object3とObject4は回収可能なオブジェクトと判断されます。

Javaでは、以下のオブジェクトがGC Rootsとなります:

  • a. 仮想マシンスタック(スタックフレーム内のローカル変数テーブル)から参照されるオブジェクト
  • b. メソッド領域の定数から参照されるオブジェクト
  • c. メソッド領域のクラス静的属性から参照されるオブジェクト
  • d. ネイティブメソッドスタック内のJNI(Nativeメソッド)から参照されるオブジェクト

参照の種類

a. 強参照(Strong Reference)
一般的なプログラムコードに存在する参照(例:Object obj = new Object())。強参照が存在する限り、GCコレクタは決して参照先のオブジェクトを回収しません。

b. 軟参照(Soft Reference)
必須ではないオブジェクト。現在のメモリ状況に応じて回収が決定されます。メモリが不足している場合は回収され、そうでない場合は保持されます。メモリオーバーフロー例外が発生した時点では、この種類のオブジェクトは存在しません。

c. 弱参照(Weak Reference)
弱参照に関連付けられたオブジェクトは、次回のGCが発生するまでしか生存できません。つまり、GCが実行されるたびに必ず回収されます。

d. 幽霊参照(Phantom Reference)
オブジェクトに幽霊参照が存在しても、その生存時間には影響しません。また、幽霊参照を通じてオブジェクトインスタンスを取得することもできません。オブジェクトに幽霊参照を設定する唯一の目的は、そのオブジェクトが回収されたときにシステム通知を受け取ることです。

finalizeメソッド

ルート検索アルゴリズムでは、到達不可能なオブジェクト(例えば上記のObject3とObject4)も「完全に死亡」したわけではありません。これは以下の2つのマーキングプロセスによって決定されます。

a. 第一次マーキング
参照チェーンが見つからない場合、最初のマーキングが行われます。この時点で第一次スクリーニングが実施され、オブジェクトがfinalizeメソッドを実行する必要があるかどうかが判断されます。オブジェクトがfinalizeメソッドをオーバーライドしていない場合、またはfinalizeメソッドがすでにJVMによって呼び出されている場合は、finalizeメソッドを実行する必要がないと見なされます。

b. 第二次マーキング
finalizeメソッドを実行する必要がある場合、オブジェクトはF-Queueキューに配置され、優先度の低いFinalizerスレッドがfinalizeメソッドの実行をトリガーします。finalizeメソッドは、オブジェクトの生死を最終的に判断する最後の機会です。この時点で、GCはキュー内のオブジェクトに対して2回目のマーキングを行います。オブジェクトがfinalizeメソッド内で自己救済(GC Rootsとの参照チェーンを確立)に成功した場合、2回目のマーキングで回収対象から除外されます。そうでない場合、オブジェクトは死亡したと判断されます。

例:オブジェクトの自己救済

1 public class ObjectRescueExample {
2     private static Object rescueHook;
3     
4     @Override
5     protected void finalize() throws Throwable {
6         super.finalize();
7         System.out.println("finalizeメソッドが実行されました");
8         rescueHook = this; // 自己救済
9     }
10     
11     public static void main(String[] args) throws Exception {
12         ObjectRescueExample instance = new ObjectRescueExample();
13         instance = null; // 強参照を解除
14         
15         System.gc(); // GCを強制実行
16         Thread.sleep(1000); // finalizeメソッドが実行されるのを待つ
17         
18         if (rescueHook != null) {
19             System.out.println("オブジェクトは自己救済に成功しました");
20         } else {
21             System.out.println("オブジェクトは回収されました");
22         }
23         
24         rescueHook = null; // 再び参照を解除
25         System.gc(); // 再度GCを実行
26         Thread.sleep(1000); // finalizeが再び呼ばれないことを確認
27         
28         if (rescueHook == null) {
29             System.out.println("2回目のGCでオブジェクトは回収されました");
30         }
31     }
32 }

実行結果は「finalizeメソッドが実行されました」「オブジェクトは自己救済に成功しました」「2回目のGCでオブジェクトは回収されました」となります。

この結果から、オブジェクトがGC時に自己救済できることがわかります。重要な点は、この自己救済は1回しかできないということです。なぜなら、オブジェクトのfinalizeメソッドはシステムによって最大1回しか自動的に呼び出されないためです。

タグ: JVM ガベージコレクション Java メモリ管理 参照型

8月23日 12:03 投稿