【概要】
SoftReference/WeakReference/PhantomReference が含まれる java.lang.ref は JDK1.2 の頃に導入されたパッケージであるが、昔から用意されている API の割にあまり使われていない。このパッケージを利用するとプログラムからガーベージコレクタとの対話を可能になるため、開発の中でヒープの動作をプログラム的に調整したい場合には便利である。

【キーワード】
リファレンス、reference、java.lang.ref、SoftReference、WeakReference、PhantomReference、ReferenceQueue、ガーベージコレクション、ガーベージコレクタ、GC、Garbage Collection、Garbage Collector

【詳細】
1. 参照オブジェクトの種類

(1) ソフト参照(SoftReference)
[リファレントの解放条件]
- リファレントが強参照されていない。
- リファレントの使用頻度とヒープの状況から、ガーベージコレクタがリファレントを解放すべきだと判断した。

[適用例]
メモリの容量に応じてスケールするキャッシュ。

[メモ]
どの程度有効なキャッシュになるかは VM の実装の賢さに依存する。
ちなみに Sun の VM の実装では、ガーベージコレクタがリファレンスごとにタイムスタンプを設定しており、リファレントが作成・参照されるたびにタイムスタンプが更新される。ガーベージコレクタはガーベージコレクションのタイミングで閾値を算出し、この閾値より古いものはリファレント解放の対象とする。

(2) 弱参照(WeakReference)
[リファレントの解放条件]
- リファレントが強参照されていない。
- リファレントがソフト参照されていない。

[適用例]
java.util.WeakHashMap: キーが弱可到達になったらエントリごと削除するマップの実装。

[メモ]
オブジェクトが使われなくなったときに、そのオブジェクトのエントリを削除する要求を明示的に行わずともエントリが削除される Map、List、Set などを実装する際に利用できる。特に有効なのは Map。

(3) ファントム参照(PhantomReference)
[リファレントの解放条件]
- リファレントが強参照されていない。
- リファレントがソフト参照されていない。
- リファレントが弱参照されていない。
- 上記条件を満たしたリファレントを持つ PhantomReference のインスタンスの clear() メソッドが明示的に呼び出された。

[適用例]
オブジェクトの finalize() メソッドが呼ばれてから実際にヒープから解放されるまでの間に何らかの処理を行う場合。ヒープの解放をトラッキングする必要があるアプリケーションを開発する際には有効。

[メモ]
ファントム参照の get() メソッドの戻り値は常に null となる。このため、リファレントについての情報を取得する場合には PhantomReference のサブクラスを作成し、そのフィールドに情報を格納しておく必要がある。AccesibleObject#setAccessible() で強引に private フィールドである refelent にアクセスしようとしている人たちもいるが、これは VM の実装によって動作する保証もないし、PhantomReference の正しい使い方ではないので、やってはいけない。

ユーザーが明示的に clear() を呼び出さない限り、どこからも参照されていないにも関わらずヒープにインスタンスが存在する状態が続くため注意が必要。

2. 参照オブジェクトの動作

(1) 各参照オブジェクトの動作の比較

[プログラム]

import java.lang.ref.*;

public class ReferenceTest {
  public static final String WEAK = "weak";

  public static final String SOFT = "soft";

  public static final String PHANTOM = "phantom";

  private static Reference createReference(Object o, String method) {
    if (method.equals(WEAK)) {
      return new WeakReference(o);
    } else if (method.equals(SOFT)) {
      return new SoftReference(o);
    } else if (method.equals(PHANTOM)) {
      return new PhantomReference(o, new ReferenceQueue());
    } else {
      throw new IllegalArgumentException("Invalid method. method="
          + method);
    }
  }

  public static void main(String[] args) throws Exception {
    //String method = WEAK; // WeakReference を使用
    //String method = SOFT; // SoftReference を使用
    String method = PHANTOM; // PhantomReference を使用

    int referenceCount = 10;
    int size = 10000000;
    Reference[] refs = new Reference[referenceCount];
    for (int i = 0; i < referenceCount; i++) {
      byte[] data = new byte[size];
      refs[i] = createReference(data, method);

      int clearedCount = 0;
      int totalCount = i + 1;
      for (int j = 0; j < totalCount; j++) {
        if (refs[j].get() == null)
          clearedCount++;
      }
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("* FreeMemory/TotalMemory = " + freeMemory + "/"
          + totalMemory + ", Cleared/Total = " + clearedCount + "/"
          + totalCount);
    }
  }
}
[実行結果]
SoftReference
* FreeMemory/TotalMemory = 1885216/12034048, Cleared/Total = 0/1
* FreeMemory/TotalMemory = 8074856/28213248, Cleared/Total = 0/2
* FreeMemory/TotalMemory = 5984184/36122624, Cleared/Total = 0/3
* FreeMemory/TotalMemory = 13896432/54034432, Cleared/Total = 0/4
* FreeMemory/TotalMemory = 3828120/54034432, Cleared/Total = 0/5
* FreeMemory/TotalMemory = 6512016/66650112, Cleared/Total = 0/6
* FreeMemory/TotalMemory = 56513888/66650112, Cleared/Total = 6/7
* FreeMemory/TotalMemory = 46429848/66650112, Cleared/Total = 6/8
* FreeMemory/TotalMemory = 36429832/66650112, Cleared/Total = 6/9
* FreeMemory/TotalMemory = 26429816/66650112, Cleared/Total = 6/10

WeakReference
* FreeMemory/TotalMemory = 1885224/12034048, Cleared/Total = 0/1
* FreeMemory/TotalMemory = 2223368/12361728, Cleared/Total = 1/2
* FreeMemory/TotalMemory = 1158384/11296768, Cleared/Total = 2/3
* FreeMemory/TotalMemory = 7298752/17436672, Cleared/Total = 3/4
* FreeMemory/TotalMemory = 1900200/12038144, Cleared/Total = 4/5
* FreeMemory/TotalMemory = 1900176/12038144, Cleared/Total = 5/6
* FreeMemory/TotalMemory = 1900152/12038144, Cleared/Total = 6/7
* FreeMemory/TotalMemory = 1900128/12038144, Cleared/Total = 7/8
* FreeMemory/TotalMemory = 1900104/12038144, Cleared/Total = 8/9
* FreeMemory/TotalMemory = 1900080/12038144, Cleared/Total = 9/10

PhantomReference
* FreeMemory/TotalMemory = 1885224/12034048, Cleared/Total = 1/1
* FreeMemory/TotalMemory = 8074808/28213248, Cleared/Total = 2/2
* FreeMemory/TotalMemory = 5984112/36122624, Cleared/Total = 3/3
* FreeMemory/TotalMemory = 13896400/54034432, Cleared/Total = 4/4
* FreeMemory/TotalMemory = 3828088/54034432, Cleared/Total = 5/5
* FreeMemory/TotalMemory = 6511936/66650112, Cleared/Total = 6/6
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

(2) ファントム参照オブジェクトのサンプル

[プログラム]

import java.lang.ref.*;

public class PhantomReferenceTest {
  public static void main(String[] args) throws Exception {
    int count = 10;
    int size = 10000000;
    ReferenceQueue queue = new ReferenceQueue();
    Reference[] refs = new PhantomReference[count];
    for (int i = 0; i < count; i++) {
      byte[] data = new byte[size];
      refs[i] = new PhantomReference(data, queue);

      Reference r;
      while ((r = queue.poll()) != null) {
        r.clear();
      }

      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("* FreeMemory/TotalMemory = " + freeMemory + "/"
          + totalMemory);
    }
  }
}
[動作結果]
* FreeMemory/TotalMemory = 1885176/12034048
* FreeMemory/TotalMemory = 8051160/28213248
* FreeMemory/TotalMemory = 8861232/28999680
* FreeMemory/TotalMemory = 8861696/28999680
* FreeMemory/TotalMemory = 8861672/28999680
* FreeMemory/TotalMemory = 8861648/28999680
* FreeMemory/TotalMemory = 8861624/28999680
* FreeMemory/TotalMemory = 8861600/28999680
* FreeMemory/TotalMemory = 8861576/28999680
* FreeMemory/TotalMemory = 8861552/28999680

3. ReferenceQueue
参照オブジェクトのコンストラクタの第二引数に ReferenceQueue インスタンスを渡すことで、参照オブジェクトを ReferenceQueue に登録することができる。ReferenceQueue に登録されている参照オブジェクトのリファレントが解放対象とみなされると、リファレントの finalize() が呼び出された後に参照オブジェクトが ReferenceQueue のキューに追加される。

(1) ReferenceQueue を用いたサンプル

[プログラム]

import java.lang.ref.*;

public class ReferenceQueueTest {
  private static class MyObject {
    public void finalize() throws Throwable {
      System.out.println("* finalized.");
      super.finalize();
    }
  }

  private static class MyObjectReference extends WeakReference {
    private long timestamp;

    public MyObjectReference(Object o, ReferenceQueue queue) {
      super(o, queue);
      this.timestamp = System.currentTimeMillis();
    }

    public long getTimestamp() {
      return timestamp;
    }
  }

  private static void checkQueue(ReferenceQueue queue) {
    System.out.println("* checking queue...");
    MyObjectReference r;
    while ((r = (MyObjectReference) queue.poll()) != null) {
      System.out.println("* r(" + r.getTimestamp() + ") is enqueued.");
    }
  }

  public static void main(String[] args) throws Exception {
    ReferenceQueue queue = new ReferenceQueue();
    Reference ref = new MyObjectReference(new MyObject(), queue);

    checkQueue(queue);
    System.out.println("* calling System.gc()...");
    System.gc();
    checkQueue(queue);
  }
}
[実行結果]
* checking queue...
* calling System.gc()...
* finalized.
* checking queue...
* r(1145274299899) is enqueued.

(2) ReferenceQueue のキューのチェック

ReferenceQueue のキューをチェックするには、次の2通りの方法がある。

- poll()
キューをポーリングし、参照オブジェクトがキューに追加されていれば参照オブジェクトを返す。 参照オブジェクトがキューに追加されていなければ null を返す。このメソッドを利用するには、キューをポーリングするスレッドを用意するか、頻繁に呼び出されるメソッドやキューの内容に関係のあるメソッド内でこのメソッドを呼び出す。

- remove() / remove(long timeout)
キューに格納されている参照オブジェクトを削除して返す。参照オブジェクトが存在しなければブロックする。