🔑 Раздел 10 · Вопрос #27

Что такое WeakHashMap

WeakHashMap — это специальная реализация интерфейса Map, которая автоматически удаляет пару «ключ-значение», когда на объект ключа больше не осталось сильных ссылок (Strong Refe...


🟢 Junior Level

WeakHashMap — это специальная реализация интерфейса Map, которая автоматически удаляет пару «ключ-значение», когда на объект ключа больше не осталось сильных ссылок (Strong References) вне самой карты, и он был собран сборщиком мусора (Garbage Collector).

Главные особенности:

  1. Слабые ссылки на ключи: Ключи оборачиваются в WeakReference. Для сборщика мусора наличие слабой ссылки не является препятствием для удаления объекта из памяти.
  2. Сильные ссылки на значения: Сами значения (values) удерживаются по сильной ссылке до тех пор, пока запись не будет удалена из карты.
  3. Ленивая очистка: Физическое удаление «мертвых» записей из внутреннего массива таблицы происходит не мгновенно при срабатывании 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;
    }
}

Жизненный цикл удаления записи:

  1. GC фаза: Сборщик мусора определяет, что на объект ключа нет обычных (Strong) ссылок.
  2. Очистка референса: GC обнуляет ссылку внутри WeakReference (ref.get() == null) и помещает сам объект Entry в связанную очередь ReferenceQueue.
  3. Метод 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)
            }
        }
    }
    
  4. Освобождение памяти: Ссылка 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):

  1. Внутренний массив table удерживает объект Entry.
  2. Поле Entry.value удерживает UserMetadata по сильной ссылке.
  3. UserMetadata.owner удерживает объект User (ключ) по сильной ссылке.
  4. Результат: Объект ключа остается достижимым из GC Roots через цепочку table -> Entry -> value -> key. Ключ никогда не станет слабодостижимым (Weakly Reachable), GC никогда не поместит его в ReferenceQueue, и запись навечно останется в куче!

Отсутствие фонового демона очистки

В WeakHashMap нет никакого фонового потока, периодически очищающего память:

  • Если вы положили в WeakHashMap 1 000 000 записей с тяжелыми значениями.
  • Занулили все внешние ссылки на ключи.
  • GC выполнил сборку мусора и поместил 1 000 000 записей в ReferenceQueue.
  • Но ваше приложение больше никогда не вызывает методы этой карты:

Все 1 000 000 объектов значений value продолжат занимать сотни мегабайт в Heap, потому что метод expungeStaleEntries() вызывается исключительно изнутри мутирующих или читающих методов самой WeakHashMap.

Почему WeakHashMap не подходит для кэширования в Production?

Многие ошибочно пытаются сделать на WeakHashMap кэш ответов API или сущностей БД. Это плохая идея:

  1. Неуправляемый жизненный цикл: Кэш очищается не по бизнес-правилам, а по воле сборщика мусора. В фазах интенсивного выделения памяти GC Young Generation будет вымывать записи через каждые 500 миллисекунд.
  2. Нет политик вытеснения: Отсутствует контроль максимального размера (Max Size), времени жизни (TTL, TTI) и алгоритмов эвикшена (LRU, W-TinyLFU).
  3. Не потокобезопасна: Требует синхронизации 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.

Связанные темы