1. 概要

この記事では、java.utilパッケージのWeakHashMapについて説明します。

データ構造を理解するために、ここではそれを使用して単純なキャッシュ実装を展開します。 ただし、これはマップがどのように機能するかを理解するためのものであり、独自のキャッシュ実装を作成することはほとんどの場合悪い考えであることに注意してください。

簡単に言うと、 WeakHashMap は、 Map インターフェイスのハッシュテーブルベースの実装であり、WeakReferenceタイプのキーを使用します。

WeakHashMap のエントリは、そのキーが通常使用されなくなると自動的に削除されます。つまり、そのキーを指す単一の参照はありません。 ガベージコレクション(GC)プロセスがキーを破棄すると、そのエントリはマップから効果的に削除されるため、このクラスは他のMap実装とは多少異なる動作をします。

2. 強、ソフト、弱参照

WeakHashMap がどのように機能するかを理解するには、 WeakReferenceクラスを調べる必要があります。これは、WeakHashMap実装のキーの基本構造です。 Javaには、3つの主要なタイプの参照があります。これについては、次のセクションで説明します。

2.1. 強力な参照

強力な参照は、日常のプログラミングで使用する参照の最も一般的なタイプです。

Integer prime = 1;

変数primeには、値1のIntegerオブジェクトへの強い参照があります。 それを指す強い参照を持つオブジェクトは、GCの対象にはなりません。

2.2. ソフトリファレンス

簡単に言うと、 SoftReference を指すオブジェクトは、JVMが絶対にメモリを必要とするまでガベージコレクションされません。

JavaでSoftReferenceを作成する方法を見てみましょう。

Integer prime = 1;  
SoftReference<Integer> soft = new SoftReference<Integer>(prime); 
prime = null;

prime オブジェクトには、それを指す強力な参照があります。

次に、primeの強い参照をソフト参照にラップします。 その強力な参照nullを作成した後、 prime オブジェクトはGCに適格ですが、JVMが絶対にメモリを必要とする場合にのみ収集されます。

2.3. 弱参照

弱参照によってのみ参照されるオブジェクトは、熱心に収集されたガベージです。 その場合、GCはメモリが必要になるまで待機しません。

次の方法で、JavaでWeakReferenceを作成できます。

Integer prime = 1;  
WeakReference<Integer> soft = new WeakReference<Integer>(prime); 
prime = null;

prime参照nullを作成すると、 prime オブジェクトは、それを指す他の強力な参照がないため、次のGCサイクルでガベージコレクションされます。

WeakReference タイプの参照は、WeakHashMapのキーとして使用されます。

3. 効率的なメモリキャッシュとしてのWeakHashMap

大きな画像オブジェクトを値として保持し、画像名をキーとして保持するキャッシュを構築するとします。 その問題を解決するための適切なマップ実装を選択したいと思います。

単純なHashMapを使用することは、値オブジェクトが大量のメモリを占有する可能性があるため、適切な選択ではありません。 さらに、アプリケーションで使用されなくなった場合でも、GCプロセスによってキャッシュから再利用されることはありません。

理想的には、GCが未使用のオブジェクトを自動的に削除できるようにするマップの実装が必要です。 大きな画像オブジェクトのキーがアプリケーションで使用されていない場合、そのエントリはメモリから削除されます。

幸い、WeakHashMapにはまさにこれらの特性があります。 WeakHashMap をテストして、どのように動作するかを見てみましょう。

WeakHashMap<UniqueImageName, BigImage> 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<UniqueImageName, BigImage> 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がトリガーされた後、マップにはimageNameSecondという1つのエントリのみが含まれます。

4. 結論

この記事では、java.util。 WeakHashMap がどのように機能するかを完全に理解するために、Javaの参照のタイプを調べました。 WeakHashMap の動作を活用する単純なキャッシュを作成し、期待どおりに機能するかどうかをテストしました。

これらすべての例とコードスニペットの実装は、 GitHubプロジェクトにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。