JavaのWeakHashMapのガイド
1. 概要
この記事では、java.utilパッケージのWeakHashMapについて説明します。
データ構造を理解するために、ここではそれを使用して簡単なキャッシュ実装を展開します。 ただし、これはマップがどのように機能するかを理解するためのものであり、独自のキャッシュ実装を作成することはほとんど常に悪い考えです。
簡単に言えば、WeakHashMapは、WeakReferenceタイプのキーを使用した、Mapインターフェースのハッシュテーブルベースの実装です。
WeakHashMapのエントリは、そのキーが通常使用されなくなると自動的に削除されます。つまり、そのキーを指す単一のReferenceはありません。 ガベージコレクション(GC)プロセスがキーを破棄すると、そのエントリはマップから効果的に削除されるため、このクラスは他のMapの実装とは多少異なる動作をします。
2. 強、ソフト、弱参照
WeakHashMapがどのように機能するかを理解するには、we need to look at a WeakReference class –これはWeakHashMap実装のキーの基本構造です。 Javaには、3つの主要なタイプの参照があります。これについては、次のセクションで説明します。
2.1. 強参照
強力な参照は、日常のプログラミングで使用する最も一般的なタイプのReferenceです。
Integer prime = 1;
変数primeには、値1のIntegerオブジェクトに対するstrong referenceがあります。 それへの強い参照ポインティングを持つすべてのオブジェクトは、GCの対象ではありません。
2.2. ソフト参照
簡単に言うと、SoftReferenceが指すオブジェクトは、JVMが絶対にメモリを必要とするまでガベージコレクションされません。
JavaでSoftReferenceを作成する方法を見てみましょう。
Integer prime = 1;
SoftReference soft = new SoftReference(prime);
prime = null;
primeオブジェクトには、それを指す強い参照があります。
次に、primeの強い参照をソフト参照にラップします。 その強力な参照nullを作成した後、primeオブジェクトはGCに適格ですが、JVMが絶対にメモリを必要とする場合にのみ収集されます。
2.3. 弱い参照
弱参照によってのみ参照されるオブジェクトは、熱心に収集されたガベージです。その場合、GCはメモリが必要になるまで待機しません。
次の方法でJavaでWeakReferenceを作成できます。
Integer prime = 1;
WeakReference soft = new WeakReference(prime);
prime = null;
prime参照nullを作成すると、primeオブジェクトは、それを指す他の強力な参照がないため、次のGCサイクルでガベージコレクションされます。
WeakReferenceタイプの参照は、WeakHashMapのキーとして使用されます。
3. 効率的なメモリキャッシュとしてのWeakHashMap
大きな画像オブジェクトを値として保持し、画像名をキーとして保持するキャッシュを構築するとします。 その問題を解決するために適切なマップ実装を選択したいと思います。
値オブジェクトは大量のメモリを占有する可能性があるため、単純なHashMapを使用することは適切ではありません。 さらに、アプリケーションで使用されなくなった場合でも、GCプロセスによってキャッシュから再利用されることはありません。
理想的には、GCが未使用のオブジェクトを自動的に削除できるようにするMap実装が必要です。 大きな画像オブジェクトのキーがアプリケーションのどこでも使用されていない場合、そのエントリはメモリから削除されます。
幸い、WeakHashMapにはまさにこれらの特性があります。 WeakHashMapをテストして、どのように動作するかを見てみましょう。
WeakHashMap map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
imageName = null;
System.gc();
await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);
BigImageオブジェクトを格納するWeakHashMapインスタンスを作成しています。 BigImageオブジェクトを値として、imageNameオブジェクト参照をキーとして配置しています。 imageNameは、WeakReferenceタイプとしてマップに保存されます。
次に、imageName参照をnullに設定します。したがって、bigImageオブジェクトを指す参照はもうありません。 WeakHashMapのデフォルトの動作は、次のGCで参照のないエントリを再利用することです。そのため、このエントリは次のGCプロセスによってメモリから削除されます。
System.gc()を呼び出して、JVMにGCプロセスをトリガーさせます。 GCサイクルの後、WeakHashMapは空になります。
WeakHashMap map = new WeakHashMap<>();
BigImage bigImageFirst = new BigImage("foo");
UniqueImageName imageNameFirst = new UniqueImageName("name_of_big_image");
BigImage bigImageSecond = new BigImage("foo_2");
UniqueImageName imageNameSecond = new UniqueImageName("name_of_big_image_2");
map.put(imageNameFirst, bigImageFirst);
map.put(imageNameSecond, bigImageSecond);
assertTrue(map.containsKey(imageNameFirst));
assertTrue(map.containsKey(imageNameSecond));
imageNameFirst = null;
System.gc();
await().atMost(10, TimeUnit.SECONDS)
.until(() -> map.size() == 1);
await().atMost(10, TimeUnit.SECONDS)
.until(() -> map.containsKey(imageNameSecond));
imageNameFirst参照のみがnullに設定されていることに注意してください。 imageNameSecond参照は変更されません。 GCがトリガーされた後、マップには1つのエントリ(imageNameSecond)のみが含まれます。
4. 結論
この記事では、java.util.WeakHashMapがどのように機能するかを完全に理解するために、Javaの参照のタイプを調べました。 WeakHashMapの動作を活用する単純なキャッシュを作成し、期待どおりに機能するかどうかをテストしました。
これらすべての例とコードスニペットの実装は、MavenプロジェクトであるGitHub projectにあるため、そのままインポートして実行するのは簡単です。