Что такое WeakHashMap
WeakHashMap — это специальная реализация интерфейса Map, которая автоматически удаляет пару «ключ-значение», когда на объект ключа больше не осталось сильных ссылок (Strong Refe...
🟢 Junior Level
WeakHashMap — это специальная реализация интерфейса Map, которая автоматически удаляет пару «ключ-значение», когда на объект ключа больше не осталось сильных ссылок (Strong References) вне самой карты, и он был собран сборщиком мусора (Garbage Collector).
Главные особенности:
- Слабые ссылки на ключи: Ключи оборачиваются в
WeakReference. Для сборщика мусора наличие слабой ссылки не является препятствием для удаления объекта из памяти. - Сильные ссылки на значения: Сами значения (
values) удерживаются по сильной ссылке до тех пор, пока запись не будет удалена из карты. - Ленивая очистка: Физическое удаление «мертвых» записей из внутреннего массива таблицы происходит не мгновенно при срабатывании GC, а лениво — при последующих обращениях к методам карты (
get,put,size).
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object(); // Сильная ссылка на ключ
map.put(key, "Metadata for Key");
System.out.println("До GC: " + map.size()); // 1
key = null; // Обнуляем сильную внешнюю ссылку
System.gc(); // Запрос GC (в учебных целях)
// При вызове size() срабатывает внутренняя очистка
System.out.println("После GC: " + map.size()); // 0 (запись автоматически удалена)
}
}
Аналогия: Представьте гардероб в отеле, где вещи гостей привязаны к электронным карточкам-ключам. Как только гость выписывается из отеля и сдает карточку (сильная ссылка исчезла), временные вещи из гардероба автоматически отправляются на утилизацию без дополнительных звонков.
🟡 Middle Level
Механизм WeakReference и ReferenceQueue
В основе архитектуры лежит наследование внутреннего узла Entry от класса WeakReference:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value; // ВНИМАНИЕ: значение удерживается по СИЛЬНОЙ ссылке!
final int hash;
Entry<K,V> next;
Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
super(key, queue); // Ключ регистрируется в очереди ReferenceQueue
this.value = value;
this.hash = hash;
this.next = next;
}
}
Жизненный цикл удаления записи:
- GC фаза: Сборщик мусора определяет, что на объект ключа нет обычных (Strong) ссылок.
- Очистка референса: GC обнуляет ссылку внутри
WeakReference(ref.get() == null) и помещает сам объектEntryв связанную очередьReferenceQueue. - Метод
expungeStaleEntries(): При каждом последующем вызове методов карты (get,put,size,remove,resize) вызывается опрос очереди:private void expungeStaleEntries() { for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { Entry<K,V> e = (Entry<K,V>) x; int i = indexFor(e.hash, table.length); // Вырезание узла e из связного списка бакета table[i] // e.value = null; (обнуление ссылки на значение для GC) } } } - Освобождение памяти: Ссылка
e.valueзануляется, позволяя сборщику мусора утилизировать тяжелый объект значения.
Сравнение уровней достижимости (Reachability) в Java
| Тип ссылки | Класс | Когда удаляется GC | Применение |
|---|---|---|---|
| Strong | Обычная ссылка T obj = new T() |
Никогда, пока достижима из GC Roots | Стандартные бизнес-объекты |
| Soft | SoftReference<T> |
Только при реальной угрозе OutOfMemoryError |
In-memory кэши данных в RAM |
| Weak | WeakReference<T> |
При любом цикле сборщика мусора | WeakHashMap, кэши метаданных |
| Phantom | PhantomReference<T> |
После финализации объекта | Очистка нативных ресурсов (Cleaner) |
🔴 Senior Level
Главная ловушка: циклическая зависимость (Value-to-Key Leak)
Самая опасная и распространенная архитектурная ошибка при использовании WeakHashMap — ситуация, когда объект значения прямо или косвенно ссылается на свой ключ:
class UserMetadata {
private final User owner; // Сильная ссылка на ключ!
public UserMetadata(User owner) { this.owner = owner; }
}
Map<User, UserMetadata> map = new WeakHashMap<>();
User user = new User("Alice");
map.put(user, new UserMetadata(user));
user = null; // Мы думаем, что ключ удалится сборщиком мусора...
System.gc();
Почему здесь возникает вечная утечка памяти (Memory Leak):
- Внутренний массив
tableудерживает объектEntry. - Поле
Entry.valueудерживаетUserMetadataпо сильной ссылке. UserMetadata.ownerудерживает объектUser(ключ) по сильной ссылке.- Результат: Объект ключа остается достижимым из GC Roots через цепочку
table -> Entry -> value -> key. Ключ никогда не станет слабодостижимым (Weakly Reachable), GC никогда не поместит его вReferenceQueue, и запись навечно останется в куче!
Отсутствие фонового демона очистки
В WeakHashMap нет никакого фонового потока, периодически очищающего память:
- Если вы положили в
WeakHashMap1 000 000 записей с тяжелыми значениями. - Занулили все внешние ссылки на ключи.
- GC выполнил сборку мусора и поместил 1 000 000 записей в
ReferenceQueue. - Но ваше приложение больше никогда не вызывает методы этой карты:
Все 1 000 000 объектов значений value продолжат занимать сотни мегабайт в Heap, потому что метод expungeStaleEntries() вызывается исключительно изнутри мутирующих или читающих методов самой WeakHashMap.
Почему WeakHashMap не подходит для кэширования в Production?
Многие ошибочно пытаются сделать на WeakHashMap кэш ответов API или сущностей БД. Это плохая идея:
- Неуправляемый жизненный цикл: Кэш очищается не по бизнес-правилам, а по воле сборщика мусора. В фазах интенсивного выделения памяти GC Young Generation будет вымывать записи через каждые 500 миллисекунд.
- Нет политик вытеснения: Отсутствует контроль максимального размера (Max Size), времени жизни (TTL, TTI) и алгоритмов эвикшена (LRU, W-TinyLFU).
- Не потокобезопасна: Требует синхронизации
Collections.synchronizedMap, что делает доступ бутылочным горлышком. Для реального продакшена следует использовать специализированные библиотеки кэширования: Caffeine или Guava Cache.
4 Tricky Questions
1. Что произойдет, если использовать строковые литералы (например, "userId") в качестве ключей в WeakHashMap?
Ответ:
Записи с такими ключами никогда не будут удалены сборщиком мусора.
Строковые литералы помещаются JVM во внутренний пул строк (String Pool), который удерживает сильные ссылки на объекты строк до тех пор, пока загрузчик классов (ClassLoader) не будет выгружен из памяти. Поскольку на строку "userId" всегда существует сильная ссылка из String Pool, она никогда не станет слабодостижимой, и WeakHashMap будет вести себя как обычная бесконечно растущая HashMap. То же самое касается закешированных объектов Integer.valueOf(10) (из пула от -128 до 127).
2. Поддерживает ли WeakHashMap красно-чёрные деревья (TreeNode) при большом числе коллизий в бакете?
Ответ:
Нет, не поддерживает.
В отличие от HashMap, которая в Java 8 была модернизирована красно-чёрными деревьями (TreeNode), класс WeakHashMap остался на старой архитектуре связных списков Java 1.2. При большом числе коллизий в одном бакете узлы Entry всегда связываются в односвязный список, и поиск деградирует до строгого линейного времени $O(N)$ независимо от размера таблицы.
3. Может ли размер WeakHashMap измениться между двумя последовательными строчками кода без вызова методов мутации?
Ответ: Да, абсолютно. Рассмотрим сценарий:
int size1 = map.size(); // 1. Вызвал expungeStaleEntries(), вернул 10
// В этот микромомент сработал GC и собрал 5 ключей!
int size2 = map.size(); // 2. Снова вызвал expungeStaleEntries(), вернул 5
Между двумя вызовами map.size() в коде не было ни одного вызова put() или remove(), однако размер карты уменьшился вдвое из-за фоновой работы сборщика мусора и ленивой очистки очереди при втором вызове.
4. Как правильно организовать двустороннюю ассоциацию ключ-значение в WeakHashMap, чтобы избежать утечки памяти?
Ответ:
Если значение обязано ссылаться на свой ключ, сильную ссылку в значении необходимо также заменить на слабую ссылку (WeakReference):
class SafeValue {
private final WeakReference<User> userRef; // Слабая ссылка предотвращает цикл!
public SafeValue(User user) {
this.userRef = new WeakReference<>(user);
}
}
В этом случае цепочка Entry -> value -> WeakReference -> key не содержит сильных ссылок на ключ, и сборщик мусора сможет беспрепятственно утилизировать ключ и саму запись.
🎯 Шпаргалка для интервью
- Суть WeakHashMap: Ключи оборачиваются в
WeakReference. Когда на ключ нет strong-ссылок извне, GC утилизирует ключ, а запись удаляется из карты. - Значения — Strong References: Значение (
value) удерживается по сильной ссылке. - Фатальная ошибка (Memory Leak): Если
valueссылается наkey, образуется сильный циклEntry -> value -> key, и запись никогда не удалится! - Ленивая очистка: Удаление записей происходит не по таймеру, а при вызове методов карты через опрос
ReferenceQueue(expungeStaleEntries()). - Ловушка литералов: Строки
"str"иIntegerот -128 до 127 закешированы JVM, имеют сильные ссылки и никогда не удаляются изWeakHashMap. - Не для кэшей: В
WeakHashMapнет TTL, LRU и лимита памяти. Для production-кэша используйте Caffeine.