LoginSignup
43
36

More than 5 years have passed since last update.

[Java] WeakReferenceとその仲間たち

Last updated at Posted at 2015-11-28

目的

  • 何度読んでも意味が分からない java.lang.ref パッケージを、忘れないように(あるいは忘れても良いように)メモ

結論

  • SoftReference はキャッシュ用(あまり使い勝手は良くない)
  • WeakReference はオブジェクトが無くなった時に後処理をしたいとか、オブジェクトがある間だけ処理をしたい(でもGC対象になっても良い)場合に使う
  • PhantomReference は効率的なファイナライズ機構用(たぶん使わない)

愚痴

いきなり愚痴ですが、Javadoc を読んでいるとソフト参照とファントム参照のどちらが弱いのか分からなくなってくることがありませんか?

各メソッドの内容を読めば、強いほうから弱いほうに以下の順番になっていることは分かるのですが。

  • 強参照
  • ソフト参照
  • 弱参照
  • ファントム参照

しかし java.lang.ref パッケージの Javadocには以下のように書いてあります。

弱いものから順に、ソフト、弱、およびファントムという3種類の参照オブジェクトが提供されます。

これだけ見ると、ファントム参照よりもソフト参照の方が弱いように読めてしまいますよね。

念のために 英語版のJavadoc を見ると以下のように書いてあります。

Three types of reference objects are provided, each weaker than the last: soft, weak, and phantom.

これなら「~最後にいくにしたがって弱くなる」なんじゃないのかな? それなら納得なんですけど。

※正しい訳については、コメント欄のご指摘を参照ください。

SoftReference

ソフト参照のJavadoc についてもよく分からないですが、ポイントは以下の部分でしょうか。

ただし、仮想マシンの実装は、最近作成されたソフト参照または最近使用されたソフト参照をクリアしないことが奨励されます。

つまり、強参照がなくなっても、最近作ったものや最近参照したものは、メモリに余裕があるなら残しておいてくれる(かもしれない)ってことですよね。

使いどころとしては、多少のコストがかかる計算の計算結果をキャッシュする、みたいなケースが考えられますね。 Javadoc にも以下のような記述がありますし。

ソフト参照は通常、メモリー・センシティブなキャッシュを実装するために使用されます。

例えば Function<T, U> のような(入力だけで出力が決まる、副作用のない)関数があったとして、キャッシュとして Map<T, SoftReference<U>> があればよさそうですね。

もちろん ReferenceQueue に溜まったソフト参照分は適宜マップから削除してあげないといけないでしょうけど。

JavaVM 毎に実装は異なる可能性はあるのですが、OracleのJavaVMの場合についてはソースを見ると以下のようになっていました(前半や一部コメントは削除しました)。

SoftReference.java
// (略)
public class SoftReference<T> extends Reference<T> {

    /**
     * Timestamp clock, updated by the garbage collector
     */
    static private long clock;

    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

インスタンス変数の timestamp に対して、ソフト参照の生成時、参照時にクラススタティックな変数 clock の値を設定しています。
clock の値は初期化も更新もされていないですが、コメントを見る限りガベージコレクターが勝手に書き換えてくれるんでしょうね。

ためしに、リフレクションで無理やり覗いてみました。

ソース:

ClockPeeping.java
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

public class ClockPeeping {

    public static void main(String[] args) throws Exception {

        System.out.println("1:" + getClock());
        TimeUnit.SECONDS.sleep(3);
        System.out.println("2:" + getClock());
        System.gc();
        System.out.println("3:" + getClock());
        TimeUnit.SECONDS.sleep(3);
        System.out.println("4:" + getClock());
        System.gc();
        System.out.println("5:" + getClock());

        System.out.println("nano:" + System.nanoTime());
    }

    public static long getClock() throws Exception {
        Class<?> clazz = SoftReference.class;
        Field clockField = clazz.getDeclaredField("clock");
        clockField.setAccessible(true);
        Long clock = clockField.getLong(null);
        return clock;
    }
}

実行結果:

1:91486589
2:91486589
3:91489686
4:91489686
5:91492690
nano:91492692564268

GC実行時のマイクロ秒( System.nanoTime() / 1000000 )が設定されているみたいですね。

おそらくはGC実施時に、閾値(GC時刻 - 適当なマイクロ秒)とソフト参照の timestamp を比較して、古い場合にはGC対象にするようになっているんでしょうね(メモリ状況によっては、ですが)。

もちろん、VMのバージョンやOSなどによっても異なる可能性はあるとして。

ただ、キャッシュとして使うにしても、どの程度残っているのか、時間やメモリ量なども調整はできないので、大規模システムなどで実用的に使うには難しい気がしますね。

小規模のスタンドアロンなアプリなどであれば、それなりに使えるかもしれませんが。

WeakReference

弱参照の使いどころは、主に以下の2つの目的でしょうか。

  • 対象のオブジェクトが無くなったら、関連する情報も削除したい
  • 対象のオブジェクトがある間は処理がしたいが、GC対象になるのは抑止したくない

「関連する情報も削除したい」というのは理解しやすいですね。 WeakHashMap とかもありますし。
といっても、この場合の「オブジェクト」というのはあまり多くない気がしますけど。

すぐに思いつくものは、以下のものぐらい?

  • スレッド
  • クラスローダー
  • クラス・メソッドなど

スレッドに関しては、専用の ThreadLocal があるので、そちらの方がよさそうですね。
他にも画面関係とか画像とかフォントとかのリソース系については使い道があるかもしれないですね。

ただ 弱参照のJavadoc に書いてある主要な目的はよく分かりませんが。

弱参照は、ほとんどの場合で正規化マッピングを実装するために使用されます。

「正規化マッピング」って何のことだろう?

英語版のJavadoc を見ると、以下のように書いてありました。

Weak references are most often used to implement canonicalizing mappings.

「カノニカル」の訳語は「正準」のはずなので、「正規化マッピング」じゃなく「正準化マッピング」ですね。意味が分からないのは同じ、あるいはさらに悪くなっていますが。

java.io.File のメソッドで getCanonicalFile() というものがあります。
これは、例えば1つのファイルのパスを表すのに /foo/bar/baz.txt/foo/bar2/../bar/baz.txt など複数の表現で表せるものから、一番シンプルな形である /foo/bar/baz.txt を取得するという意味ですよね。

また、HTMLの <link rel="canonical" href="~"> は、ほぼ同一の内容のページが複数URL存在する場合に、本体となるURLを示すのに使うものだと思います。

つまり、ほぼ同じ内容が複数ある場合に、その本体(カノニカルという言葉の本来の意味からすると「正典(Canon)」)を1つ用意して、それに寄せることが「正準化」になるのだと思います。

java.lang.String にある intern() みたいなことですね。
とおもったら、String#intern() のJavadoc にも「カノニカルなんちゃら」と書いてありますね。

Returns a canonical representation for the string object.

日本語版もちゃんと「正準表現」と書いてある。

文字列オブジェクトの正準表現を返します。

確かに、正準化に WeakReference は使えると思いますが「ほとんどの場合で正規化マッピングを実装するために使用されます」は言い過ぎな気がしますね。

(あるいは「canonicalizing mappings」の意味を取り違えているのかもしれませんが。可能性大)

PhantomReference

一番使いどころが分からないのがファントム参照ですね。
GC 処理の詳細を理解することがポイントなんでしょうか。

ファントム参照の前に、弱参照の復習をします。
WeakReference の Javadoc に以下のように書いてあります。

弱参照オブジェクトです。弱参照オブジェクトは、その弱参照オブジェクトのリファレントがファイナライズ可能になり、ファイナライズされ、そして再生されることを阻止することはありません。

英語版も見てみます。

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed.

つまり、GC の処理の中では以下の3つの手順があるわけですよね。

  1. ファイナライズ可能になる(finalizable)
  2. ファイナライズされる(finalized) - 必要なら finalize() を呼び出す
  3. 再生される(reclaimed) - おそらくメモリを回収すること

そして、弱参照が null を返すようになるのは、該当のオブジェクトが finalizable になるのと同時で、また ReferenceQueue に入るのは、それ以降のどこかになっています。

その時点で、ガベージ・コレクタは、そのオブジェクトへの弱参照すべてと、強参照およびソフト参照のチェーンを経由してそのオブジェクトに到達できるような、ほかの弱到達可能なオブジェクトへの弱参照すべてを、原子的にクリアします。同時に、ガベージ・コレクタは以前に弱到達可能なオブジェクトがすべてファイナライズ可能であることを宣言します。
同時にまたはあとで、ガベージ・コレクタは、参照キューに登録されているそれらの新しくクリアされた弱参照をキューに入れます。

これは、ソフト参照についても、ほぼ同じですね。

ようやくファントム参照の話ですが、ファントム参照が ReferenceQueue に入るのは、該当のオブジェクトがファイナライズされた(finalized)後、再生される(reclaimed)前になっています。

ファントム参照オブジェクトです。ファントム参照オブジェクトがキューに入れられるのは、キューに入れておかないとそれらのリファレントが再生される可能性があるとコレクタが判断したときです。

また、ファントム参照を明示的にクリアしない限りは、再生(reclaimed)をさせないことになっています。

ちょっと動作を確認してみます。

PhantomTest.java
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class PhantomTest {
    private static final ReferenceQueue<HeavyObject> queue = new ReferenceQueue<>();

    public static void main(String[] args) throws Exception {
        int n = 0;
        check(n++);

        HeavyObject obj = new HeavyObject();
        WeakReference<HeavyObject> weak = new WeakReference<>(obj, queue);
        PhantomReference<HeavyObject> phantom = new PhantomReference<>(obj, queue);

        System.out.println("1:" + weak);
        System.out.println("1:" + phantom);
        check(n++);

        obj = null; // 強参照を削除

        for (int i = 0; i < 5; i++) {
            System.gc(); // 強制GC
            TimeUnit.SECONDS.sleep(3);
            check(n++);
        }

        phantom.clear();
        System.out.println("CEARE");

        for (int i = 0; i < 3; i++) {
            System.gc(); // 強制GC
            TimeUnit.SECONDS.sleep(3);
            check(n++);
        }
    }

    private static void check(int num) {
        StringBuilder buff = new StringBuilder();
        buff.append(num);
        buff.append(":CHECK:");
        // 確保済みメモリ
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memoryBean.getHeapMemoryUsage();
        buff.append("memory=");
        buff.append(usage.getUsed());
        // GC 回数
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            buff.append(',');
            buff.append(gcBean.getName());
            buff.append('=');
            buff.append(gcBean.getCollectionCount());
        }
        System.out.println(buff.toString());

        Reference<?> ref;
        while ((ref = queue.poll()) != null) {
            System.out.println(num + ":POLL:" + ref);
        }
    }

    /** 重たいオブジェクト */
    public static class HeavyObject {

        private byte[] buff;

        public HeavyObject() {
            buff = new byte[100 * 1024 * 1024];
            Arrays.fill(buff, (byte) 0xff);
        }

        @Override
        protected void finalize() {
            System.out.println("finalize() - begin");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                // ignore
            }
            System.out.println("finalize() - end");
        }
    }
}

実行結果は以下のようになりました。

0:CHECK:memory=707720,Copy=0,MarkSweepCompact=0
1:java.lang.ref.WeakReference@139a55
1:java.lang.ref.PhantomReference@1db9742
1:CHECK:memory=105390200,Copy=1,MarkSweepCompact=1
finalize() - begin
2:CHECK:memory=108109376,Copy=1,MarkSweepCompact=2
2:POLL:java.lang.ref.WeakReference@139a55
finalize() - end
3:CHECK:memory=108111104,Copy=1,MarkSweepCompact=3
4:CHECK:memory=106704984,Copy=1,MarkSweepCompact=4
4:POLL:java.lang.ref.PhantomReference@1db9742
5:CHECK:memory=106704984,Copy=1,MarkSweepCompact=5
6:CHECK:memory=106704984,Copy=1,MarkSweepCompact=6
CEARE
7:CHECK:memory=1847408,Copy=1,MarkSweepCompact=7
8:CHECK:memory=1716320,Copy=1,MarkSweepCompact=8
9:CHECK:memory=1243152,Copy=1,MarkSweepCompact=9

想定どおり、弱参照は finalize() が終わらないうちに ReferenceQueue に入っています。

また、ファントム参照は finalize() が終わってしばらくしてから ReferenceQueue に入りました。また PhantomReference#clear() を呼び出さない間は FullGC が起きてもメモリが解放されていません。

弱参照やソフト参照と比べると、ファントム参照は finalize() が確実に終わった後に ReferenceQueue に入れられる、というのが最大の特徴でしょうか。

結局、ファントム参照のJavadocにあるように、使いどころは独自のファイナライズ機構の構築なんでしょうね。

ファントム参照オブジェクトは、ほとんどの場合、Javaのファイナライズ・メカニズムよりも柔軟な方法で、プリモルテム・クリーンアップ・アクションのスケジューリングを行うために使用されます。

いや、なにを言っているかさっぱりですが。

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

「pre-mortem」は「生前」「死亡前」という意味らしい。
つまり、事前(pre-mortem)に後処理(cleanup action)を実施している場合などを考慮したファイナライズ機構を作れるということですね。

通常 finalize() メソッドが定義されているオブジェクトは、GCがもういらない(finalizable)と判定すると、ファイナライザー・スレッドに後処理(finalized)を依頼して、その後に再度 GC が実行された際にメモリの回収を行う(reclaimed)ことになります。
つまり、最低でも2回のGCが発生しないとメモリの回収ができないわけですね。

しかし、事前に close() やら dispose() で後処理をしているなら、わざわざ finalize() の処理をする必要がないはず。

そこで、後処理が必要なクラスは finalize() メソッドを定義せずに、インスタンス生成時(あるいは後処理が必要になった時)に PhantomReference を作っておきます。
もし close()dispose() などの後処理メソッドを呼び出されれば PhantomReference#clear() を呼び出しておいて、最初の GC でメモリ回収可能にしておけばよい。

もし、後処理を呼び出されなかったら ReferenceQueue に入るので、それをすばやく検知して後処理を実施するというわけですね。

つまりすごく簡単に言えば、 C# の GC.SuppressFinalize() みたいなことが(すごく頑張れば)できる、と思えばいいのかな。

ただ PhantomReference#clear() を呼び出さないと、メモリが解放されないということは、たまに ReferenceQueue をチェックするぐらいではマズそうですね。
専用の後処理スレッド(つまり独自のファイナライザー・スレッド)を用意してあげる必要がありそうに思えます。

結構大掛かりになりそうなので、この機構を使う価値があるのは大規模なフレームワークやミドルウェアなどのレイヤーなのではないかと思います。
いや、個別に使ったっていいんでしょうけどね。

【追記】 Java9で追加された java.lang.ref.Cleaner を使うと独自のファイナライザー・スレッドは用意しないでもいいみたいですね。

43
36
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
36