Что такое WeakHashMap
Если на объект-ключ больше нет сильных ссылок (StrongReference) во всей программе, сборщик мусора (GC) автоматически утилизирует этот ключ при очередной сборке, а соответствующа...
🟢 Junior Level
1. Что такое WeakHashMap
WeakHashMap — это специализированная реализация интерфейса Map, в которой ключи хранятся с использованием слабых ссылок (WeakReference).
Если на объект-ключ больше нет сильных ссылок (StrongReference) во всей программе, сборщик мусора (GC) автоматически утилизирует этот ключ при очередной сборке, а соответствующая запись («ключ — значение») удаляется из мапы:
Map<Object, String> weakMap = new WeakHashMap<>();
Object key = new Object(); // Создана сильная ссылка на ключ
weakMap.put(key, "Метаданные объекта");
System.out.println(weakMap.size()); // 1
key = null; // Сильная ссылка уничтожена, осталась только слабая внутри WeakHashMap
System.gc(); // Рекомендация JVM запустить GC
// При следующем обращении запись исчезает:
System.out.println(weakMap.size()); // 0
2. Главное назначение: хранение сопутствующих метаданных
Основная сфера применения WeakHashMap — привязка дополнительных атрибутов или временных метаданных к объектам, жизненным циклом которых управляет внешний код.
Пример: Ассоциирование состояния UI-виджетов или прав доступа с сущностью User, пока этот объект активен в сессии:
WeakHashMap<SocketChannel, SessionContext> sessions = new WeakHashMap<>();
// Как только сокет закрыт и удален из сетевого селектора, сессия вычищается сама
🟡 Middle Level
1. Механика работы под капотом: ReferenceQueue и expungeStaleEntries()
Вопреки распространенному заблуждению, сборщик мусора не удаляет записи из массива table напрямую. JVM и GC ничего не знают о внутренних структурах коллекций.
Механизм очистки работает следующим образом:
- Нода мапы расширяет
WeakReference: Внутренний классEntry<K,V>наследуетWeakReference<Object>и при создании регистрирует ключ в очередиReferenceQueue: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: Когда сборщик обнаруживает, что на ключ нет сильных ссылок, он обнуляет ссылку внутри
WeakReference(referent = null) и помещает сам объектEntryвReferenceQueue. - Ленивая очистка (Lazy Expunge): В
WeakHashMapнет фонового потока-демона. Очистка происходит пассивно при вызове любого публичного метода (get,put,remove,size). Метод вызывает приватную процедуру: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); // Извлечение ноды из цепочки бакета table[i] // Обнуление ссылки на e.value для освобождения памяти! } } }
2. Смертельная ловушка: циклическая ссылка из Value в Key
Если объект-значение (value) прямо или косвенно хранит сильную ссылку на свой ключ (key), механизм автоматической сборки мусора полностью ломается:
class OrderMetadata {
private final Order order; // Сильная ссылка на ключ!
public OrderMetadata(Order order) { this.order = order; }
}
WeakHashMap<Order, OrderMetadata> map = new WeakHashMap<>();
Order order = new Order(123);
map.put(order, new OrderMetadata(order)); // ОШИБКА АРХИТЕКТУРЫ
order = null; // Сильная ссылка снаружи обнулена!
System.gc();
// УТЕЧКА ПАМЯТИ: запись НИКОГДА не будет удалена!
System.out.println(map.size()); // 1
Цепочка сильных ссылок:
WeakHashMap.table $\to$ Entry $\to$ Entry.value (OrderMetadata) $\to$ Order (Key).
Поскольку Order достижим по сильной ссылке из массива table, GC считает его живым и никогда не поместит Entry в ReferenceQueue.
🔴 Senior Level
1. Архитектурный антипаттерн: почему WeakHashMap НЕЛЬЗЯ использовать как кэш
Один из самых грубых архитектурных антипаттернов — построение кэша приложения на WeakHashMap.
Причины непригодности для кэширования:
- Агрессивность сборки: Слабые ссылки очищаются при каждом Minor GC (сборке в Young Generation), если на объект нет сильных ссылок. В нагруженном микросервисе Minor GC происходит каждые сотни миллисекунд. Если вы закэшировали результат тяжелого SQL-запроса по ключу-параметру, как только метод отработает и ключ выйдет из стека, при первом же GC кэш испарится. Hit rate такого кэша будет стремиться к нулю.
- Проблема интернированных ключей: Если ключами кэша являются строки-литералы (
"USER_123"), объектыLongили кэшированныеInteger(от -128 до 127), они жестко удерживаются в String Pool или кэшах оберток JVM. Они никогда не будут собраны GC, превращаяWeakHashMapв обычную неограниченнуюHashMapс утечкой памяти. - Отсутствие политик вытеснения: В
WeakHashMapнет поддержки eviction-стратегий (LRU, LFU, TTL, размерных квот).
[!TIP] Для реализации надежных кэшей в Java необходимо использовать специализированные библиотеки (например, Caffeine Cache с алгоритмом Window TinyLFU) либо структуры на
SoftReference, которые GC утилизирует только при реальной нехватке памяти в JVM.
2. Скрытая утечка памяти при отсутствии вызовов (Idle Map Leak)
Поскольку очистка expungeStaleEntries() вызывается исключительно при обращении к методам мапы, возникает эффект «мертвой памяти»:
- Если в
WeakHashMapбыло добавлено 1 000 000 записей с тяжелымиvalue(например, byte-массивами), а затем мапа перестала опрашиваться (наступил простой сервиса), GC удалит ключи, но массивEntry[] tableбудет продолжать удерживать все тяжелые объектыvalueчерез полеe.value. - Память под
valueосвободится только тогда, когда к мапе обратятся снова или когда сама мапа станет недостижимой. - По этой причине метод
size()вWeakHashMapне возвращает полеsizeза $O(1)$, а сначала вызываетexpungeStaleEntries()и пересчитывает оставшиеся элементы.
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«
WeakHashMap— это хэш-таблица, в которой ключи хранятся черезWeakReference. Когда на объект-ключ не остается сильных ссылок, GC утилизирует ключ и помещает внутреннюю нодуEntryв ассоциированнуюReferenceQueue. Очистка записей происходит пассивно (lazy) при последующих вызовах методов мапы (expungeStaleEntries()). Основное назначение — хранение сопутствующих метаданных для объектов внешнего жизненного цикла.WeakHashMapкатегорически не подходит для кэширования из-за моментального вымывания данных при Minor GC и подвержена утечкам памяти при наличии ссылок изvalueвkey».
3 каверзных вопроса на интервью с ответами
1. Почему добавление записи с ключом "some_string" в WeakHashMap может привести к неудаляемой записи?
Ответ: Строковые литералы размещаются в String Pool и имеют постоянную сильную ссылку от системного загрузчика классов (ClassLoader). Поскольку на такой ключ всегда существует сильная ссылка в JVM, он никогда не станет кандидатом на сборку мусора, и запись из WeakHashMap никогда не будет удалена автоматически.
2. Как и почему сильная ссылка из объекта-значения на объект-ключ ломает работу WeakHashMap?
Ответ: Внутренний массив бакетов table[] сильно ссылается на Entry, который сильно ссылается на поле value. Если value сильно ссылается на key, образуется непрерывная цепочка сильных ссылок от корневого объекта мапы до ключа (table -> Entry -> value -> key). В результате ключ всегда остается сильно достижимым (Strongly Reachable), GC не может поместить его WeakReference в ReferenceQueue, и запись зависает в памяти навсегда.
3. Есть ли в WeakHashMap фоновый поток для очистки мертвых записей?
Ответ: Нет. В WeakHashMap отсутствует какой-либо фоновый поток-демон. Очистка мертвых записей осуществляется «лениво» (on-demand) внутри метода expungeStaleEntries(), который опрашивает ReferenceQueue.poll() в начале каждого обращения к мапе (get, put, size, remove). Если мапа простаивает, значения value продолжают занимать память в Heap.
Типичные ошибки и Red Flags
- ❌ Использование
WeakHashMapдля кэша данных: Данные исчезают при первой же сборке мусора. Для кэшей нужен Caffeine или Ehcache. - ❌ Хранение примитивных оберток в качестве ключей:
Integer.valueOf(10)закэширован JVM и никогда не удалится. - ❌ Предположение, что значения (
values) тоже хранятся слабо: Слабыми являются только ключи; значения удерживаются по сильной ссылке до вызоваexpungeStaleEntries(). - ❌ Утверждение о потокобезопасности:
WeakHashMapне синхронизирован. Параллельное чтение и запись приведут к повреждению связных списков бакетов.