Статический кэш растёт без ограничений несмотря на сборщик мусора
Обработчик запросов кэширует каждый разобранный Session, чтобы повторные обращения были быстрыми. Сервис работает сутками и в итоге падает с OutOfMemoryError: Java heap space, хотя сборщик мусора отрабатывает постоянно, а каждый Session перестаёт использоваться задолго до падения.
Ограничения:
- повторные обращения к активной сессии должны остаться быстрыми — просто удалить кэш нельзя
- считайте
Sessionкрупным объектом, на который после завершения запроса не ссылается ничто другое
public class SessionRegistry {
private static final Map<String, Session> CACHE = new HashMap<>();
public static Session lookup(String id) {
return CACHE.computeIfAbsent(id, SessionRegistry::parse);
}
private static Session parse(String id) { /* дорого */ }
}
Найдите и исправьте ошибку.
Статическая final карта — это GC root, держащий сильную ссылку на каждый разобранный Session, поэтому ни одна запись не становится недостижимой и сборщик не может её освободить: это утечка по достижимости, а не сбой GC. Лечится ограничением кэша — LRU через LinkedHashMap.removeEldestEntry или WeakHashMap, чьи записи очищаются, как только не остаётся сильных ссылок на ключ.
- ✗Считать, что сборщик мусора делает утечку памяти в Java невозможной
- ✗Винить сборщик или размер поколений, когда объект по-прежнему достижим
- ✗Считать
System.gc()или увеличение кучи лекарством от неограниченного кэша
- →Как heap dump покажет, какой именно GC root удерживает сессии?
- →Когда
WeakHashMap— неверный кэш, а ограниченный LRU — верный?
Решение
public class SessionRegistry {
private static final int MAX_ENTRIES = 10_000;
private static final Map<String, Session> CACHE =
Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Session> eldest) {
return size() > MAX_ENTRIES; // ограничиваем кэш: вытесняем самый старый
}
});
public static Session lookup(String id) {
return CACHE.computeIfAbsent(id, SessionRegistry::parse);
}
}
Диагноз. Сборщик мусора освобождает только недостижимые объекты. CACHE — static final поле, то есть GC root: он держит сильную ссылку на каждый когда-либо разобранный Session. Объекты достижимы, значит, они по определению не мусор, и сборщик обязан их сохранить. Куча растёт монотонно, System.gc() и увеличение -Xmx лишь отодвигают падение.
Идея исправления. Утечка лечится не настройкой GC, а ограничением удержания:
- Ограниченный LRU (
LinkedHashMapв access-order +removeEldestEntry) — жёсткий потолок на размер, горячие сессии остаются в кэше. WeakHashMap— запись исчезает, как только на ключ не осталось сильных ссылок; годится, когда кто-то ещё держит живые ключи.SoftReference-значения — очищаются под нехваткой памяти; предохранитель, но не замена ограничению.