Java: オブジェクトが GC で回収されるまで待つには

あるオブジェクトが GC で回収されるまで待つにはどうしたらいいのだろうか。
普通はそんなことしなくていいように書くのがベストだが、どうしても必要な場面というのもある。WeakReference を使った機能のテストケースを書きたい場合とか。

最初に思いつくやつ

WeakReference を使えば、オブジェクトが GC で回収されたかどうかを判定できる。
WeakReference を使った機能のテストに WeakReference を使うというのはどうなんだ、という気もするが、他に使えるものもないしやむを得ない……。

        Thread t = new Thread();
        WeakReference<Thread> r = new WeakReference<>(t);
        t = null; // 強参照を破棄

        while (true) {
            if (r.get() == null) {
                break;
            }

            System.out.println("まだかなまだかなー");
            TimeUnit.MILLISECONDS.sleep(100);
            
            System.gc(); // ささやき - いのり − えいしょう - ねんじろ!
        }
        
        System.out.printf("t: %s%n", t);

この実装例では、100 ミリ秒ごとに WeakReference#get() を呼んで、1 行目で生成した Thread オブジェクトへの弱参照が無効になったかどうかを判定している。System.gc() を呼ぶのはよくない習慣とされているが、ここでは実験のためということでご容赦いただきたい。
手元の環境で動かしてみたら、

まだかなまだかなー
t: null

と表示された。無事に回収されたようだ。
……しかしこれ、よく見ると、

            if (r.get() != null) {
                break;
            }

のところで、強参照が一度復活している。get() から値が返されてから null チェックされるまでのわずかな間とはいえ、GC の邪魔をしていることになる。
もっといい方法はないだろうか。

もっといいようなそうでもないようなやり方はこれだ!

こういう場合に使うためのクラスが、実は Java API には最初から用意されている。java.lang.ref.ReferenceQueue というのがそれ。API リファレンスを調べるときは、このクラス自体の説明だけでなく、java.lang.ref パッケージの方にも説明がある。
WeakReference のコンストラクタに ReferenceQueue を指定しておくと、その WeakReference が参照している対象が GC で回収されたときに、WeakReference が ReferenceQueue の中に飛び込んでくる、という不思議な動きをする。
うまく説明できないので、さっそく実装例をどうぞ。

        Thread t = new Thread();
        ReferenceQueue<Thread> q = new ReferenceQueue<>();
        WeakReference<Thread> r = new WeakReference<>(t, q);
        t = null;

        while (true) {
            if (q.poll() != null) {
                break;
            }

            System.out.println("まだかなまだかなー");
            TimeUnit.MILLISECONDS.sleep(100);
            
            System.gc(); // ささやき - いのり − えいしょう - ねんじろ!
        }
        
        System.out.printf("t: %s, r.get(): %s%n", t, r.get());

Thread が回収されるまでは、ReferenceQueue#poll() を呼んでも null が返されるだけなのだが、回収された後で同じメソッドを呼ぶと、2 行目で作った WeakReference が取れる。したがって、poll() の戻り値が null でなくなるまで待てば、Thread が回収されたことを確認できるのだ。
手元の環境で動かしてみたら、こんな感じに出力された。

まだかなまだかなー
まだかなまだかなー
t: null, r.get(): null

ループを回る回数が 1 回増えている。回収されればすぐに null を返すようになる WeakReference#get() と違って、ReferenceQueue#poll() の方は少し遅れるかもしれないとのこと。
そうそう、さっき「WeakReference が ReferenceQueue の中に飛び込んでくる」と書いたけど、正確には、「ガーベッジコレクタが WeakReference を ReferenceQueue に追加する」らしい。GC がオブジェクトに値を追加する、というのはなんか意表をつかれた感があるが、java.lang.ref パッケージは全体的に色々特別扱いされていて、そのおかげで便利な機能を実現できている。

難しい

ReferenceQueue は、使い方が直感的とは言いづらいように思う。そのため、ドキュメントを呼んでも使い方がいまいちピンとこない人も多いんじゃないだろうか (少なくとも僕はそうだった)。
先日、ReferenceQueue の使い方を調べていた時、はてなダイアリーで id:satosystems さんという方が書かれた下記の実装例を見た。

obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
new WeakReference(obj, queue);
do {
    Reference ref = queue.poll();
    if (ref == null) {
        break;
    }
    ref.enqueue();
    Thread.sleep(100);
} while (true);
satosystemsの日記 - WeakReference と ReferenceQueue

「自分で enqueue() を呼んで WeakReference を ReferenceQueue に入れて、取れなくなるまで待つ」ってこと? えっ、これでも動くの? ReferenceQueue の使い方ってどっちが正しいの?
……と悩んだんだけど、おそらくこの実装例では、最初に作った Object が回収されたかどうかではなく、WeakReference のインスタンスが回収されたかどうかを調べてしまっているのではないかと思う。java.lang.ref パッケージのドキュメントを見ると「Reference から ReferenceQueue を参照するけど、ReferenceQueue から Rerefence を参照してはいないので、Reference を保持するのはプログラム側の責任だよ」「Reference が GC で回収されたら ReferenceQueue にも入らなくなるよ」というような趣旨のことが書いてある。
先ほども書いた通り、WeakReference の場合は、対象のオブジェクトが回収されたときに WeakReference を ReferenceQueue に入れるのはガーベッジコレクタの役目なので、僕らの側で WeakReference の enqueue を呼ぶ必要はないんだと思われる。
ちなみに、API リファレンスで Reference#enqueue() の説明を見ると、「このメソッドは Java プログラムから呼ぶために用意してあるけど、ガーベッジコレクタはこのメソッドを使用せずに直接 ReferenceQueue をいじるよ」みたいなことが書いてあって、なんかこのメソッドが何のためにあるのはよくわからない……。

References (ここでは参考文献の意)

今回主に参考にさせて頂いたサイト・記事。

Java™ Platform, Standard Edition 7 API 仕様
http://docs.oracle.com/javase/jp/7/api/
Java弱参照メモ(Hishidama's Java Weak reference Memo)
http://www.ne.jp/asahi/hishidama/home/tech/java/weak.html
satosystemsの日記 - WeakReference と ReferenceQueue
http://d.hatena.ne.jp/satosystems/20130606/1370513070
von Ewigkeit - ReferenceQueue に enqueue されるタイミング
http://d.hatena.ne.jp/Ewigkeit/20080823/1219463052