1. Обзор

В этой статье мы рассмотрим WeakHashMap из пакета java.util.

Чтобы понять эту структуру данных, мы реализуем простой кэш. Однако имейте в виду, что это предназначено для понимания того, как работает WeakHashMap, и создание собственной реализации кэша почти всегда является плохой идеей.

Проще говоря, WeakHashMap - это реализация интерфейса Map на основе хеш-таблицы с ключами типа WeakReference.

Запись в WeakHashMap будет автоматически удалена, когда ее ключ больше не используется в обычном режиме, а это означает, что нет единой ссылки, указывающей на этот ключ. Когда сборщик мусора (GC) отбрасывает ключ, его запись эффективно удаляется с карты, поэтому этот класс ведет себя несколько иначе, чем другие реализации Map.

2. Сильные, мягкие и слабые ссылки

Чтобы понять, как работает WeakHashMap, нам нужно взглянуть на класс WeakReference, который является базовой конструкцией для ключей в реализации WeakHashMap. В Java у нас есть три основных типа ссылок, которые хорошо объясняются в следующих разделах.

2.1. Сильные ссылки

Сильная ссылка - это наиболее распространенный тип ссылок, который мы используем в повседневном программировании.

Integer prime = 1;

Переменная prime имеет сильную ссылку на объект Integer со значением 1. Любой объект, имеющий сильную ссылку на него, не подходит GC.

2.2. Мягкие ссылки

Проще говоря, объект, на который указывает SoftReference, не будет собирать мусор до тех пор, пока JVM не будет абсолютно необходима память.

Давайте посмотрим, как мы можем создать SoftReference в Java:

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

Объект prime имеет сильную ссылку, указывающую на него.

Далее мы оборачиваем простую сильную ссылку в мягкую ссылку. После того как эта сильная ссылка пуста, объект prime подходит для GC, но будет собираться только тогда, когда JVM необходима память.

2.3. Слабые ссылки

Объекты, на которые ссылаются только слабые ссылки, сборщик мусора не думая, и в этом случае он не будет ждать, пока ему не понадобится память.

Мы можем создать WeakReference в Java следующим образом:

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

Когда мы установили нулевую первичную ссылку, первичный объект будет собираться мусором в следующем цикле GC, так как на него нет других сильных ссылок.

Ссылки типа WeakReference используются в качестве ключей в WeakHashMap.

3. WeakHashMap как эффективный кэш памяти

Допустим, мы хотим создать кеш, в котором объекты больших изображений хранятся в качестве значений, а имена изображений - в качестве ключей. Мы хотим выбрать правильную реализацию карты для решения этой проблемы.

Использование простого HashMap не будет хорошим выбором, потому что объекты-значения могут занимать много памяти. Более того, они никогда не будут возвращены из кэша процессом GC, даже если они больше не используются в нашем приложении.

В идеале нам нужна реализация Map, которая позволяет 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);

Мы создаём экземпляр WeakHashMap, который будет хранить наши объекты BigImage. Мы помещаем объект BigImage в качестве значения и ссылку на объект imageName в качестве ключа. ImageName будет храниться на карте как тип WeakReference.

Затем мы устанавливаем ссылку на imageName равной нулю, поэтому больше нет ссылок, указывающих на объект 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.

4. Заключение

В этой статье мы рассмотрели типы ссылок в Java, чтобы полностью понять, как работает java.util.WeakHashMap. Мы создали простой кеш, который использует поведение WeakHashMap и проверим, работает ли он так, как мы ожидали.