Що таке 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-віджетів або контексту мережевої сесії із сутністю, поки цей об’єкт активний у програмі:
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 для звільнення пам'яті! e.value = null; } } }
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
Ланцюжок сильних посилань:
\(\text{GC Root} \longrightarrow \text{WeakHashMap.table} \longrightarrow \text{Entry} \overset{\text{Strong}}{\longrightarrow} \text{OrderMetadata (Value)} \overset{\text{Strong}}{\longrightarrow} \text{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, W-TinyLFU, TTL, лімітів за розміром або пам’яттю).
[!TIP] Для реалізації надійних кешів у Java необхідно використовувати спеціалізовані бібліотеки (наприклад, Caffeine Cache з алгоритмом Window TinyLFU) або структури на
SoftReference, які GC утилізує тільки при реальній загрозі браку пам’яті (OOM) в JVM Heap.
2. Прихований витік пам’яті за відсутності викликів (Idle Map Leak)
Оскільки очищення expungeStaleEntries() викликається виключно при зверненні до методів мапи, виникає ефект «мертвої пам’яті»:
- Якщо у
WeakHashMapбуло додано 1 000 000 записів із важкимиvalue(наприклад, byte-масивами), а потім мапу перестали опитувати (настав простій сервісу), GC видалить ключі, але масивEntry[] tableпродовжуватиме утримувати всі важкі об’єктиvalueчерез полеe.value. - Пам’ять під
valueзвільниться лише тоді, коли до мапи звернуться знову або коли сама мапа стане недосяжною для GC Roots. - З цієї причини метод
size()уWeakHashMapне повертає закешоване поле за $O(1)$, а спочатку викликаєexpungeStaleEntries()і перераховує наявні живі елементи.
4 Tricky Questions
1. Чому додавання запису з рядковим літералом "session_token" у WeakHashMap може призвести до незнищенного запису?
Відповідь:
Рядкові літерали автоматично інтернуються в JVM String Constant Pool, розташований у Heap. Об’єкти в String Pool утримуються постійними сильними посиланнями від системного завантажувача класів (ClassLoader). Оскільки на такий ключ завжди існує сильне посилання в JVM, він ніколи не стане кандидатом на збирання сміття, і запис із WeakHashMap ніколи не буде видалено автоматично.
2. Як і чому сильне посилання з об’єкта-значення на об’єкт-ключ ломає роботу WeakHashMap?
Відповідь:
Внутрішній масив бакетів table[] сильно посилається на Entry, який сильно посилається на поле value. Якщо value сильно посилається на key, утворюється неперервний ланцюжок сильних посилань від кореневого об’єкта мапи до ключа (table -> Entry -> value -> key). У результаті ключ залишається сильно досяжним (Strongly Reachable), GC не може очистити слабке посилання та помістити його в ReferenceQueue, і запис назавжди зависає в пам’яті.
3. Чи є у WeakHashMap фоновий потік-демон для очищення мертвих записів?
Відповідь:
Ні. У WeakHashMap повністю відсутній будь-який фоновий потік. Очищення мертвих записів здійснюється суто «ледаче» (on-demand) всередині методу expungeStaleEntries(), який опитує ReferenceQueue.poll() на початку виклику публічних операцій (get, put, size, remove). Якщо звернень до мапи немає, важкі значення value продовжують займати пам’ять у купі.
4. Чому метод size() у WeakHashMap не працює за $O(1)$ як у HashMap?
Відповідь:
У HashMap розмір зберігається в лічильнику size, що повертається миттєво за $O(1)$. У WeakHashMap ключі можуть бути зібрані збирачем сміття асинхронно без відома самої структури даних. Якби size() просто повертав числове поле, воно містило б застарілі та вже мертві записи. Тому WeakHashMap.size() спочатку викликає expungeStaleEntries() для очищення черги від мертвих нод, а потім перераховує фактично збережені живі елементи.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«
WeakHashMap— це хеш-таблиця, у якій ключі обгорнуті уWeakReference. Коли на об’єкт-ключ не залишається сильних посилань, GC утилізує його і поміщає нодуEntryв асоційовану чергуReferenceQueue. Очищення записів відбувається ледаче під час наступних викликів методів мапи черезexpungeStaleEntries(). Основне призначення — зберігання супутніх метаданих для об’єктів зовнішнього життєвого циклу.WeakHashMapкатегорично не підходить для кешування через миттєве вимивання даних при Minor GC і схильна до витоків пам’яті при циклічних посиланнях ізvalueвkey».
Архітектурні факти високого рівня
- Слабкими є тільки ключі: Значення утримуються за сильними посиланнями до виклику
expungeStaleEntries(), де явно виконуєтьсяe.value = null. - Ледаче витіснення (Lazy Eviction): Немає фонових прибиральників; очищення черги відбувається лише під час викликів
get(),put(),remove(),size(). - Вразливість до петель посилань: Ланцюг $\text{Key} \leftarrow \text{Value}$ вимикає слабку досяжність, назавжди блокуючи запис у пам’яті.
- Антипатерн кешування: Слабкі посилання агресивно очищаються на кожному Minor GC; для продакшн-кешів слід використовувати Caffeine.
Типові помилки та Red Flags
- ❌ Використання
WeakHashMapдля кешу даних: Дані вимиваються при першому ж збиранні сміття, знижуючи hit rate до нуля. - ❌ Зберігання рядкових літералів або примітивних обгорток як ключів:
"string"абоInteger.valueOf(10)ніколи не видаляться через кеші JVM. - ❌ Припущення, що значення (
values) теж слабкі: Значення утримуються сильно, доки мапа не буде опитана. - ❌ Твердження про потокобезпечність:
WeakHashMapне синхронізована; паралельні операції пошкоджують структуру зв’язаних списків у бакетах.