最近加わったように見せて実はすごい昔から存在している java.lang.ref.Reference とその実装クラスである PhantomReference, SoftReference, WeakReference の 3 つ。今日はこいつらの動作について軽く見ていく。
ただその前に。java6 の幾つからかはシランけど jvisualvm なる jvm の挙動を GUI で見れるツールが提供されている。
とりあえず、下記の何の意味も無いコードを走らせたときの jvisualvm の挙動はこんな感じ。
long c = 0; List<BigDecimal> list = new ArrayList<BigDecimal>(); while (true) { Random r = new Random(); long l = r.nextLong(); list.add(new BigDecimal(l)); c++; }
当然のごとくヒープが食いつぶされていくのと、じわじわと CPU の稼動が GC の活動とイコールになっていくのが見て取れます。まぁ当然っすな。
空気を読んで適当に弱参照オブジェクトをクリアする SoftReference
Java の「強到達可能な」「ソフト到達可能な」「弱到達可能な」「ファントム到達可能な」のちゃんとした定義は Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle などを読んでもらうとして。ただ、いかんせんこのドキュメントはヒジョーにわかりにくい。というのも『「ソフト到達可能な」オブジェクトとは、強到達可能ではないが、ソフト参照をトラバースすることで到達できるオブジェクト』とか書いてあったりして、定義の説明に定義の説明が含まれるという構造になっているからである。ただまぁ、純技術的に厳密な定義をしようとするとこうなるのはごく普通のこと。javadoc が特別いじわるな記述というわけではない。
というわけで、ココのエントリーでは自分の言葉でコイツらを(自分にとって)わかりやすく説明しなおしてます。つまり、正確な表現ではない可能性があるのであしからず、ってコトです。
ちょっと横道にそれましたが、まずは一番わかりやすい SoftReference から。javadoc を見ると、あるオブジェクトがソフト到達可能になったら GC 対象になる、とかそんなよーなことが書いてある。ソフト到達可能ってなんぞ、って話ですが、おおよそ弱参照と考えてよい。で、弱参照のうち GC が「こいつ消していいだろ」って判定されたらそいつがソフト到達可能、ということのようだ。
と、いうわけで。下のようなコードを実行し、jvisualvm で様子を見てみる。
import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class SoftReferenceTest { public static void main(String[] args) throws InterruptedException { new SoftReferenceTest().doMain(); } private void doMain() throws InterruptedException { while (true) { m(); } } private List<Reference<Byte[]>> list = new ArrayList<Reference<Byte[]>>(); private void m() { for (int i=0; i<10; i++) { Byte[] t = new Byte[10000]; list.add(new SoftReference<Byte[]>(t)); } } }
list に無限にテキトーなサイズのオブジェクトをガンガン突っ込んでいくだけのコードです。フツーに考えると即 OutOfMemoryError になりそうなもんですが、そうはならずヒープはパンパンになってすぐ空いて、を繰り返す動作になります。list に突っ込まれるオブジェクトは、m メソッドのループの間だけは強参照ですが、それを抜けると list オブジェクトからのみしか参照が無くなる。ということはつまり、誰も使わなさそう=こいつ消していいだろ、と判定されるわけです。で、実際、テキトーなタイミングで Reference が指してる Byte[] のオブジェクトは消されているのが jvisualvm で見て取れます。
実はこのプログラム、長時間放置すると OutOfMemoryError 出すんですが……それはまた後で触れる予定。
フツーの弱参照 WeakReference
これはコードと jvisualvm の動き見た方が速いので。
import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class WeakReferenceTest { public static void main(String[] args) throws InterruptedException { new WeakReferenceTest().doMain(); } private void doMain() throws InterruptedException { while (true) { m(); } } private List<Reference<Byte[]>> list = new ArrayList<Reference<Byte[]>>(); private void m() { for (int i=0; i<10; i++) { Byte[] t = new Byte[10000]; list.add(new WeakReference<Byte[]>(t)); } } }
SoftReference が WeakReference になっただけなんですけれど。動作はかなり変わってきます。
SoftReference と違って、GC がテキトーなタイミングでオブジェクトを消してまわるわけではないのが大きな違い。ちゃんとした定義は要 javadoc 参照ですが、弱参照を消すのに GC の独自判断が介在するのかどうかが SoftReference と WeakReference の一番の違いでしょう。
で、コレだけ見ると WeakReference の使いどころがわからなさげです。
まず、SoftReference は GC の賢さに依存するのでそこがネックになる場合には使えない。まぁ……並の業務アプリでは大して問題にならないと思うんで、たいていのケースは SoftReference 使っとけばおkだとは思うんですが。
また、Reference オブジェクトそのものは回収されないことにも注意したい。これは WeakReference も SoftReference も同じだけども。
private ConcurrentHashMap<K, SoftReference<V>> map = new ConcurrentHashMap<K, SoftReference<V>>();
安直に考えるなら↑みたいにすればキャッシュが実現できる。んでまぁ、実際んところ、これは上手くいく。が、これは OutOfMemoryError の危険性を孕んでいる。というのも先に述べたように Reference オブジェクトが指しているオブジェクトは null になるものの、Reference オブジェクトそのものは map に溜まり続けるからである。とはいえ、相当量の Reference オブジェクトを溜め込まない限り OutOfMemoryError にはならんので、問題が表面化することはなさそうではあるけれど。
となると、キーが弱参照になったら値も GC される Map があればいいのに……とは、みんなが思うところなわけです。というわけで Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle が用意されている。
というわけで、使ってみる。
import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; public class WeakHashMapTest { public static void main(String[] args) throws InterruptedException { new WeakHashMapTest().doMain(); } private void doMain() throws InterruptedException { while (true) { m(); System.out.println(map.size()); } } private Map<String, Byte[]> map = new WeakHashMap<String, Byte[]>(); private void m() throws InterruptedException { for (int i=0; i<10; i++) { Byte[] v = new Byte[10000]; TimeUnit.MILLISECONDS.sleep(30); String k = String.valueOf(System.currentTimeMillis()); map.put(k, v); } } }
環境にも依るんでしょうが、map のサイズは 0 〜 100 の間くらいを行ったり来たりする。
ヒープの動きも、なるほどといった感じ。
キーが SoftReference なのは無いのかな? と一瞬思ったけど、GC のゆるふわ判断で値が消えたり消えなかったりになる Map はさすがに無いんだろなーと思いなおした。
つかいどころのわからない PhantomReference
ぶっちゃけ用途不明なのが PhantomReference である。javadoc によると、ファントム参照とは『「ファントム到達可能な」オブジェクトとは、強到達可能でも、ソフト到達可能でも、弱到達可能でもなく、ファイナライズされており、ファントム参照がそれを指しているオブジェクト』ということが書いてある。
要は、オブジェクトの finalize と実際の GC でヒープがクリアされる処理との間でなんか処理をカマしたい場合に使うもの、ということのようだ。フツーの業務アプリでそこまでヒープ領域を意識した作りこみをするケースってのが想定できないんですよね……ゲームとかだと必要になるんですかねぇ?
PhantomReference については Java Programming Tips:java.lang.ref パッケージの利用方法(SoftReference/WeakReference/PhantomReference) がかなりよくまとまっています。PhantomReference だけじゃなくて SoftReference, WeakReference についてもキッチリした解説が載ってます。