S-JIS[2008-07-14] 変更履歴

Java弱参照クラス

通常のインスタンスは、どこかの変数が保持(参照)していれば、GCの対象にならない。
しかし弱参照にして保持すると、他の通常の参照が全て無くなれば、GCの対象になる。
(弱参照という用語を使う場合は、通常の参照は「強参照」と呼ぶ。強参照と弱参照の間に当たるソフト参照、弱参照より下のファントム参照というのもあるらしい)


WeakHashMap

WeakHashMapは、キーを弱参照で保持するHashMap
このマップのキーに当たるオブジェクトの(他からの)強参照が全て無くなると、このマップ内からそのキー(と値)が削除される。

例:

	Map<Integer, String> map = new WeakHashMap<Integer, String>();

	// キーを強参照で保持しつつ、マップに値をセット
	Integer[] force = new Integer[10];
	for (int i = 0; i < force.length; i++) {
		force[i] = new Integer(i);	//強参照としてキーに当たる値を保持
		map.put(force[i], String.valueOf((char) ('A' + i)));
	}

	// マップの初期状態を表示
	System.out.println(map.toString());
	System.out.println("↓");

	force[1] = null;	// 強参照を無くしてみる

	System.gc();	// GCを実行
	//Thread.sleep(10);

	// GC実行後の状態を表示
	System.out.println(map.toString());

実行結果:

{9=J, 8=I, 7=H, 6=G, 5=F, 4=E, 3=D, 2=C, 1=B, 0=A}
↓
{9=J, 8=I, 7=H, 6=G, 5=F, 4=E, 3=D, 2=C, 0=A}

GCが実行されると、強参照が無くなったオブジェクトは回収される。
WeakHashMapはそれを検知し、マップ内からそのエントリー(キーと値)を削除するようになっている。

ここでは例として自分でGCを実行したが、普通は適当な時点でGCが実行されるので、その後で(要するに不定期で)WeakHashMap内のデータが変更される。
強参照が無くなっても、GCが実行されないとデータは消されない。


WeakHashMapでは(というかWeakReference全般で)キーのセット方法を間違えると、思ってもみない動作になってしまう。

使用失敗例:

	Map<String, Integer> map = new WeakHashMap<String, Integer>();

	String[] force = new String[10];
	for (int i = 0; i < force.length; i++) {
		force[i] = "Key" + i;	// 強参照として保持
		map.put("Key" + i, i);
	}

	System.out.println(map.toString());
	System.out.println(map.get("Key1"));
	System.out.println("↓");

	// 強参照forceは特に消さない

	System.gc();
	// Thread.sleep(10);

	System.out.println(map.toString());

実行結果:

{Key9=9, Key7=7, Key8=8, Key5=5, Key6=6, Key3=3, Key4=4, Key1=1, Key2=2, Key0=0}
1
↓
{}

最初の例と同じ感じで強参照を残しているはずなのに、今回はマップの中身が消えてしまった
つまり、全キーが「強参照は無くなった」と判断されてしまった訳だ。

これは、強参照として保持しているオブジェクトと、WeakHashMap内のキーのオブジェクトが異なる為。
どちらも「"Key" + i」という同じ値を使っているように見えるが、Stringの結合の実体はStringBuilder#append().toString()であり、内部では「new String()」が使われている。すなわち、値としては同じ(equals()は真)だけれども、オブジェクト(インスタンス・参照)は異なる。
したがって、強参照で保持しているつもりでも全く異なるオブジェクトだったので、意味が無かったのだ。


ThreadLocal

ThreadLocalは、スレッドID(スレッドのオブジェクト)をキーに、自由に値を保持できるMapのようなもの。

これもスレッドオブジェクトを弱参照で保持して、WeakHashMapと同様に処理している。
(スレッドが無くなったら(終了したら)、保持していた値も破棄する)


WeakReference

WeakReferenceは、弱参照としてオブジェクトを保持する為のクラス。

例:

	//Integerオブジェクトの弱参照を保持する
	Integer integer = new Integer(123456);
	WeakReference<Integer> wr = new WeakReference<Integer>(integer);

	//WeakReferenceの中身を取得・表示
	Integer i = wr.get();
	System.out.println(i);
	System.out.println("↓");

	//強参照を全て無くす
	integer = null;
	i       = null;
	System.gc();

	//GC実行後に中身を取得・表示
	i = wr.get();
	System.out.println(i);

実行結果:

123456
↓
null

保持したいオブジェクト(参照)をWeakReferneceのコンストラクターに渡すと、そのオブジェクトはWeakRefernece内で弱参照として保持される。
中身を取得したい場合はget()を使う。

強参照が全て無くなると、GCが実行された際に(ファイナライザーが実行され、)WeakReference内がクリアされる。
つまり、その後のget()ではnullが返る(nullしか返らない)。


複数のWeakReferenceを使う場合、どれがクリアされたかを知るには、全WeakReferenceをリストか何かに入れておいて一つ一つget()してnullが返ってくるかどうかをチェックすればいいような気がする。
しかしこれは無駄が多い。

そこで、もう少しエレガントな方法が用意されている。
参照キュー(ReferenceQueueクラス)というものを用意し、WeakReferenceのコンストラクターに渡す。
すると、WeakReferenceから値が消されると、参照キューにそのWeakReferenceが追加される。

	ReferenceQueue<Integer> queue = new ReferenceQueue<Integer>();

	List<Integer> force = new ArrayList<Integer>();
	List<WeakReference<Integer>> rlist = new ArrayList<WeakReference<Integer>>();

	for (int i = 0; i < 4; i++) {
		Integer n = new Integer(1000 + i);
		WeakReference<Integer> wi = new WeakReference<Integer>(n, queue);
		rlist.add(wi);

		System.out.printf("%d: %s%n", n, wi);

		if (i % 2 == 0) {
			// 今回の例として、偶数のときだけ強参照を残す
			force.add(n);
		}
	}

	// GCを実行
	System.gc();
	System.out.println("↓");

	// 参照キューに入ったものを表示
	for (;;) {
		Reference<? extends Integer> r = queue.poll();
		if (r == null) break;

		System.out.println("poll: " + r);
//		System.out.println(r.get()); //nullが出力されるのみ
	}
1000: java.lang.ref.WeakReference@de6f34
1001: java.lang.ref.WeakReference@156ee8e
1002: java.lang.ref.WeakReference@47b480
1003: java.lang.ref.WeakReference@19b49e6
↓
poll: java.lang.ref.WeakReference@156ee8e
poll: java.lang.ref.WeakReference@19b49e6
  備考
強参照 Int1000 Int1001 Int1002 Int1003 この例では、強参照は
ローカル変数nや
forceというリスト。
弱参照
wref0
Int1001
wref1
Int1001
wref2
Int1002
wref3
Int1003
キュー  

↓奇数だけ強参照を無くす

  備考
強参照 Int1000 null Int1002 null もしここでwref3.get()を呼び出したら、
Int1003が返る。
もしここでpoll()を呼び出したら、
nullが返る。
弱参照
wref0
Int1001
wref1
Int1001
wref2
Int1002
wref3
Int1003
キュー  

↓GCが動く

  備考
強参照 Int1000 null Int1002 null 強参照が無くなったWeakReferenceの中身がクリアされ、
キューに入れられる。
弱参照
wref0
Int1001
wref1
null
wref2
Int1002
wref3
null
キュー wref1wref3

poll()を一回実行

  備考
強参照 Int1000 null Int1002 null キューから1つだけWeakReferenceが返される。
(返されたWeakRefはキュー内から消える)
弱参照
wref0
Int1001
wref1
null
wref2
Int1002
wref3
null
キュー wref1
poll() wref3 もしwref3.get()を実行したら、nullが返る。

poll()をもう一回実行

  備考
強参照 Int1000 null Int1002 null キューから1つだけWeakReferenceが返される。
(返されたWeakRefはキュー内から消える)
弱参照
wref0
Int1001
wref1
null
wref2
Int1002
wref3
null
キュー  
poll() wref1

なお、WeakReferenceインスタンス自体をどこかに強参照して残しておかないと、poll()で取得することは出来ない。
この例ではrlistにわざわざ保持している。

しかしrlistから削除されるようには出来ていないので、無駄なWeakReferenceが残ってしまう。
WeakRefereneceの中身がクリアされるタイミングで呼ばれるメソッドとかがあれば、それをオーバーライドして…ということが出来ると思うのだが、そういう仕組みにはなっていない。
WeakRefereneceにはclear()というメソッドがあるが、これはプログラマーが明示的にクリアしたい時に呼ぶものであって、クリア時に呼ばれるものではない。残念。

WeakHashMapでは、size()やget()等の中でまずpoll()を呼び、使われなくなった弱参照を削除している。

同様に、使われなくなった弱参照を削除するリスト(WeakList)を作ってみました。
(削除されなかった弱参照を全て取得することも出来るので、後始末が必要なデータの保持に使えるかも)


Java目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま