Тема 6. Memory and Garbage Collector
Память в Java это фундамент производительности и стабильности. Большая часть проблем в продакшене связана не с синтаксисом, а с лишними ссылками, неверной моделью жизненного цикла объектов и неправильными ожиданиями от Garbage Collector.
Базовая модель памяти
stack и heap решают разные задачи:
stackхранит кадры вызовов методов: локальные переменные и ссылки;heapхранит сами объекты и массивы;- после завершения метода его stack frame удаляется автоматически;
- объект в heap живет, пока достижим по ссылкам.
int age = 30; // примитивное значение
String name = "Alex"; // ссылка на объект в heap
Как работает Garbage Collector
GC очищает память по достижимости (reachability), а не по таймеру.
Упрощенный алгоритм:
- JVM определяет корневые точки (
GC Roots): активные потоки, локальные переменные в стеке, статические поля и т.д. - От корней проходит граф ссылок и помечает достижимые объекты как живые.
- Недостижимые объекты считаются мусором и могут быть удалены.
- При необходимости память уплотняется, чтобы снизить фрагментацию.
Ключевой вывод: вы не контролируете точный момент удаления объекта. System.gc() только просьба к JVM, а не команда с гарантией.
Поколенческая модель памяти
Большинство современных GC в JVM опираются на идею поколений:
Young Generation: сюда попадают новые объекты;Old Generation: долгоживущие объекты, пережившие несколько сборок;Metaspace: метаданные классов (не сами объекты).
Почему это работает: в типичном приложении много короткоживущих объектов, и их выгодно собирать чаще и быстрее.
Виды GC-пауз простыми словами
Minor GC: чаще, обычно короче, работает в young generation.Major/Full GC: реже, обычно тяжелее, может затрагивать old generation и давать заметные паузы.
Если приложение начинает «подвисать», часто причина в слишком частых или слишком долгих паузах GC.
Частые причины утечек памяти в Java
Утечка в Java это ситуация, когда GC исправен, но объекты остаются достижимыми из-за ошибочной логики ссылок.
Основные паттерны:
staticколлекции, которые бесконечно растут.- Кеш без лимита и без стратегии удаления.
- Подписки/слушатели без отписки.
- ThreadLocal без очистки в долгоживущих потоках.
- Хранение тяжелых объектов в singleton-компонентах без необходимости.
Пример:
public class CacheHolder {
private static final List<byte[]> CACHE = new ArrayList<>();
public static void add(byte[] data) {
CACHE.add(data); // рост без ограничений
}
}
Здесь данные остаются достижимыми через CACHE, поэтому GC не имеет права их удалить.
Как писать код, дружественный к GC
- Ограничивайте размер кешей и очередей.
- Освобождайте подписки и ресурсы явно (
close,unsubscribe). - Не держите ненужные ссылки в долгоживущих объектах.
- Избегайте создания лишних временных объектов в горячих циклах.
- Для крупных буферов продумывайте жизненный цикл заранее.
Мини-практика для диагностики
- Следите за графиками heap usage и GC pause time.
- При росте памяти снимайте heap dump и ищите «кто держит ссылку».
- Проверяйте dominator tree: какие объекты удерживают наибольший объем.
- Сверяйте поведение с нагрузкой: утечка обычно проявляется как «ступенчатый рост» памяти без возврата к базовому уровню.
Что важно запомнить
- GC удаляет только недостижимые объекты.
- Проблемы памяти чаще вызваны архитектурой ссылок, а не «плохим GC».
- Контроль памяти это часть проектирования данных, кешей и жизненного цикла компонентов.