Javaの4種類の参照タイプとその特性

1. 強参照 (Strong Reference)

強参照はプログラムコード中最も一般的に存在する参照で、Object obj = new Object() のような形式です。強参照が存在する限り、ガベージコレクタは参照されているオブジェクトを回収しません。

強参照には以下の3つの特徴があります:

  • 強参照は直接ターゲットオブジェクトにアクセスできます。
  • 強参照が指すオブジェクトは、システムがOOM例外をスローするまで回収されません。
  • 強参照はメモリリークを引き起こす可能性があります。例えば、Listに追加されたnewオブジェクトは、Listが回収されると、内部のオブジェクトがアクセスできなくなるにもかかわらず回収されません。

FinalReferenceクラスの完全な定義は以下の通りです:

package java.lang.ref;
/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

2. 軟参照 (Soft Reference)

軟参照は、まだ役立つが必須ではないオブジェクトを記述するために使用されます。軟参照に関連付けられたオブジェクトは、メモリが十分な場合はガベージコレクタによって回収されません。メモリが不足している場合は、これらのオブジェクトが回収されます。

システムがOutOfMemoryErrorを発生させようとする前に、JVMは軟参照に関連付けられたオブジェクトを回収対象にリストアップし、2回目の回収を実行します。この回収後もメモリが不足している場合にのみ、システムはOutOfMemoryErrorをスローします。

JDK 1.2からSoftReferenceクラスが提供され、軟参照を実装します。参照キュー(ReferenceQueue)と連携してメモリに敏感なキャッシュを実現します。軟参照が参照するオブジェクトがガベージコレクタによって回収されると、JVMはその軟参照を関連付けられた参照キューに追加します。

2.1 例1: 軟参照のガベージコレクション

package com.example.java.ref;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class SoftReferenceDemo {
    private static ReferenceQueue<DataObject> softQueue = new ReferenceQueue<>();

    private static class DataObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("DataObjectのfinalizeが呼び出されました");
        }
        @Override
        public String toString() {
            return "私はDataObjectです";
        }
    }

    private static class QueueMonitor implements Runnable {
        Reference<DataObject> ref = null;
        @Override
        public void run() {
            try {
                ref = (Reference<DataObject>) softQueue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ref != null) {
                System.out.println("SoftReferenceのオブジェクトは " + ref.get());
            }
        }
    }
    
    public static void main(String[] args) {
        DataObject obj = new DataObject();
        SoftReference<DataObject> softRef = new SoftReference<>(obj, softQueue);
        new Thread(new QueueMonitor()).start();

        // 強参照を削除しないと、objは回収されません
        obj = null;
        System.gc();
        System.out.println("GC後: Soft Get = " + softRef.get());
        System.out.println("大きなメモリを割り当てようとしています...");
        byte[] b = new byte[5 * 1024 * 725];
        System.out.println("byte[]作成後: Soft Get = " + softRef.get());
        System.gc();
    }
}

テスト方法1:

VMパラメータを-Xmx5Mに設定してテストメソッドを実行すると、以下の結果が得られます:

GC後: Soft Get = 私はDataObjectです
大きなメモリを割り当てようとしています...
byte[]作成後: Soft Get = 私はDataObjectです
DataObjectのfinalizeが呼び出されました
SoftReferenceのオブジェクトは null

コードの説明:

  • まずDataObjectオブジェクトを構築し、object変数に割り当て、強参照を形成します。
  • 次にSoftReferenceを使用してこのDataObjectオブジェクトの軟参照softRefを構築し、softQueue参照キューに登録します。softRefが回収されると、softQueueキューに追加されます。
  • obj = nullを設定してこの強参照を削除すると、システム内のDataObjectオブジェクトへの参照は軟参照のみになります。
  • GCを明示的に呼び出し、軟参照のget()メソッドを使用してDataObjectオブジェクトの参照を取得すると、オブジェクトが回収されていないことがわかります。これはGCがメモリが十分な場合、軟参照オブジェクトを回収しないことを示しています。
  • 次に大きなヒープスペース5*1024*725を要求すると、システムのヒープメモリが不足し、新しいGCサイクルが発生します。このGC後、softRef.get()はDataObjectオブジェクトを返さず、nullを返します。これはメモリが不足している場合、軟参照が回収されることを示しています。軟参照が回収されると、登録された参照キューに追加され、マルチスレッドのsoftQueue.remove()がブロックしなくなり、プログラムが正常に終了します。

2.2 例2: 軟参照キャッシュの使用

public class ImageCacheManager {
    private Map cacheMap = new HashMap<>();

    // Bitmapの軟参照をHashMapに保存
    public void cacheImage(String path) {
        // 強参照のBitmapオブジェクト
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 軟参照のBitmapオブジェクト
        SoftReference<Bitmap> softBitmap = new SoftReference<>(bitmap);
        // Mapに追加してキャッシュ
        cacheMap.put(path, softBitmap);
        // 使用後、ビットマップオブジェクトを明示的にnullに設定
        bitmap = null;
    }

    public Bitmap getImage(String path) {
        // キャッシュから軟参照のBitmapオブジェクトを取得
        SoftReference<Bitmap> softBitmap = cacheMap.get(path);
        // 軟参照の存在を確認
        if (softBitmap == null) {
            return null;
        }
        // Bitmapオブジェクトを取得。メモリ不足でBitmapが回収された場合はnullが取得されます
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}

2.3 軟参照の適用シナリオ

軟参照は主にメモリに敏感な高速キャッシュに使用されます。Androidシステムで頻繁に使用されます。ほとんどのAndroidアプリケーションは大量の画像を使用します。ファイルの読み取りはハードウェア操作が必要で速度が遅いため、画像をキャッシュし、必要なときに直接メモリから読み取ることを検討します。

しかし、画像はメモリスペースを多く占有するため、多くの画像をキャッシュするとOutOfMemoryErrorが発生しやすくなります。この問題を回避するために軟参照技術を使用できます。

SoftReferenceはOOMの問題を解決できます:各オブジェクトを軟参照でインスタンス化すると、このオブジェクトはキャッシュ形式で保存されます。このオブジェクトを再度呼び出すとき、軟参照のget()メソッドを通じて直接オブジェクトのリソースデータを取得できます。メモリがOOMを発生しようとしている場合、GCはすべての軟参照を迅速にクリアし、OOMを防止します。

3. 弱参照 (Weak Reference)

弱参照は非必須のオブジェクトを記述するために使用されます。その強度は軟参照よりも弱く、弱参照に関連付けられたオブジェクトは次のガベージコレクションが発生するまで生存できます。ガベージコレクタが動作すると、現在のメモリが十分かどうかに関わらず、弱参照のみに関連付けられたオブジェクトが回収されます。弱参照オブジェクトがガベージコレクタによって回収されると、登録された参照キューに追加されます。

前のセクションの例1のコードを少し変更します:

package com.example.java.ref;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class WeakReferenceDemo {
    private static ReferenceQueue<DataObject> weakQueue = new ReferenceQueue<>();

    private static class DataObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("DataObjectのfinalizeが呼び出されました");
        }
        @Override
        public String toString() {
            return "私はDataObjectです";
        }
    }

    private static class QueueMonitor implements Runnable {
        Reference<DataObject> ref = null;
        @Override
        public void run() {
            try {
                ref = (Reference<DataObject>)weakQueue.remove();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(ref != null) {
                System.out.println("削除された弱参照: " + ref);
                System.out.println("弱参照のオブジェクト ref.get() は: " + ref.get());
            }
        }
    }

    public static void main(String[] args) {
        DataObject object = new DataObject();
        Reference<DataObject> weakRef = new WeakReference<>(object,weakQueue);
        System.out.println("作成された弱参照: " + weakRef);
        new Thread(new QueueMonitor()).start();

        object = null;
        System.out.println("GC前: Weak Get = " + weakRef.get());
        System.gc();
        System.out.println("GC後: Weak Get = " + weakRef.get());
    }
}

デモ方法:

JVMパラメータを変更しない場合、実行結果は以下の通りです:

作成された弱参照: java.lang.ref.WeakReference@6f94fa3e
GC前: Weak Get = 私はDataObjectです
GC後: Weak Get = null
削除された弱参照: java.lang.ref.WeakReference@6f94fa3e
しかし弱参照のオブジェクト ref.get() は: null
DataObjectのfinalizeが呼び出されました

わかる点:

  • GC前、弱参照オブジェクトはガベージコレクタによってまだ回収されていないため、weakRef.get()を通じて対応するオブジェクト参照を取得できます。
  • しかしGCを実行すると、弱参照は発見され、すぐに回収され、登録された参照キューに追加されます。この時点でweakRef.get()を通じてオブジェクトの参照を取得しようとすると失敗します。

4. 虚参照 (Phantom Reference)

虚参照はゴースト参照またはファントム参照とも呼ばれ、最も弱い参照関係です。虚参照を持つオブジェクトは、参照がないのとほとんど同じで、いつでもガベージコレクタによって回収される可能性があります。

虚参照のget()メソッドを通じて強参照を取得しようとすると、常に失敗します。また、虚参照は必ず参照キューと一緒に使用され、その目的はガベージコレクションプロセスを追跡することです。

虚参照のget()メソッドは常にnullを返します。その実装は以下の通りです:

public T get() {
    return null;
}

第2節の例1のコードをさらに変更します:

package com.example.java.ref;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;

public class PhantomReferenceDemo {
    private static ReferenceQueue<DataObject> phantomQueue = new ReferenceQueue<>();

    private static class DataObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("DataObjectのfinalizeが呼び出されました");
        }
        @Override
        public String toString() {
            return "私はDataObjectです";
        }
    }

    private static class QueueMonitor implements Runnable {
        Reference<DataObject> ref = null;
        @Override
        public void run() {
            try {
                ref = (Reference<DataObject>) phantomQueue.remove();
                System.out.println("削除された虚参照: " + ref);
                System.out.println("しかし虚参照のオブジェクト ref.get() は: " + ref.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DataObject object = new DataObject();
        Reference<DataObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("作成された虚参照: " + phantomRef);
        new Thread(new QueueMonitor()).start();

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "回GC");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

デモ方法:

JVMパラメータを変更しない場合、実行結果は以下の通りです:

作成された虚参照: java.lang.ref.PhantomReference@6f94fa3e
第1回GC
DataObjectのfinalizeが呼び出されました
第2回GC
削除された虚参照: java.lang.ref.PhantomReference@6f94fa3e
しかし虚参照のオブジェクト ref.get() は: null

わかる点:

  • 1回目のGC後、システムはゴミオブジェクトを見つけ、finalize()メソッドを呼び出してメモリを回収しますが、すぐに虚参照オブジェクトを回収キューに追加しません。
  • 2回目のGC時、このオブジェクトが実際にGCによってクリアされ、この時点で虚参照オブジェクトが虚参照キューに追加されます。

虚参照の使用シナリオ

虚参照の最大の目的はオブジェクト回収を追跡し、破棄されたオブジェクトに関連するリソースをクリーンアップすることです。

通常、オブジェクトが使用されなくなると、そのクラスのfinalize()メソッドをオーバーライドしてオブジェクトのリソースを回収できます。しかし、不適切な使用はオブジェクトの復活を引き起こす可能性があります。例えば、このようにfinalize()メソッドをオーバーライドすると:

public class Test {
    private static Test obj;
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        obj = this;
    }
}

Testオブジェクトを作成: obj = new Test();、その後obj = null;とし、System.gc()を呼び出してオブジェクトを破棄しようとします。

しかし残念ながら、何回System.gc()を呼び出しても効果はありません。再度obj = null;を実行しない限り、オブジェクトは回収されません。

理由:**JVMは各オブジェクトに対してオーバーライドされたfinalize()メソッドを最大1回しか実行しません。サンプルコードでは、super.finalize()の後にobjに再割り当てを行い、objが復活したため、そのオーバーライドされたfinalize()メソッドは2回目には呼び出されません。**

虚参照によるオブジェクトクリーンアップ

上記の小さなコードスニペットは、finalize()メソッドをオーバーライドすることが非常に信頼できないことを示しています。虚参照を使用してオブジェクトが占有するリソースをクリーンアップできます。コードを以下のように変更します:

package com.example.java.ref;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class PhantomReferenceCleanup {
    private static ReferenceQueue<DataObject> phantomQueue = new ReferenceQueue<>();
    private static Map resourceMap = new HashMap<>();

    private static class DataObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("DataObjectのfinalizeが呼び出されました");
        }
        @Override
        public String toString() {
            return "私はDataObjectです";
        }
    }

    private static class ResourceCleaner implements Runnable {
        Reference<DataObject> refObj = null;

        @Override
        public void run() {
            try {
                refObj = (Reference<DataObject>) phantomQueue.remove();
                // リソースMapから弱参照オブジェクトを削除し、手動でリソースを解放
                Object value = resourceMap.get(refObj);
                System.out.println("リソースをクリーンアップ: " + value);
                resourceMap.remove(refObj);

                System.out.println("削除された虚参照: " + refObj);
                System.out.println("虚参照のオブジェクト obj.get() は: " + refObj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DataObject object = new DataObject();
        Reference<DataObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("作成された虚参照: " + phantomRef);
        new Thread(new ResourceCleaner()).start();
        // 作成された虚参照オブジェクトをリソースMapに保存
        resourceMap.put(phantomRef, "いくつかのリソース");

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "回GC");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

実行結果:

作成された虚参照: java.lang.ref.PhantomReference@6f94fa3e
第1回GC
DataObjectのfinalizeが呼び出されました
第2回GC
リソースをクリーンアップ: いくつかのリソース
削除された虚参照: java.lang.ref.PhantomReference@6f94fa3e
しかし虚参照のオブジェクト obj.get() は: null

タグ: Java ガベージコレクション 参照タイプ SoftReference WeakReference

6月7日 21:24 投稿