Память и сборщик мусора в Java: что реально происходит под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как Java управляет памятью: не просто «сборка мусора»

Многие разработчики, переходящие на Java с C++ или Python, сначала радуются: «Ура, не нужно думать об утечках памяти!». А потом получают OutOfMemoryError в самый неподходящий момент и понимают — управление памятью в Java требует глубокого понимания. Сегодня мы заглянем под капот JVM и разберём, как на самом деле работают память и сборщик мусора в Java.

Эта статья будет полезна как новичкам, которые хотят разобраться в Java memory model, так и опытным разработчикам, которые ищут способы оптимизации garbage collection Java. Поехали!


Как устроена память в Java: куча, стек и Metaspace

Прежде чем говорить о сборщике мусора Java, нужно понять, где именно «живут» ваши объекты. JVM делит память на несколько ключевых областей:

  • Heap (куча) — здесь хранятся все создаваемые объекты. Это основное поле работы garbage collection Java.
  • Stack (стек) — хранит примитивные типы и ссылки на объекты в куче. Каждый поток имеет свой стек.
  • Metaspace — заменил PermGen в Java 8, хранит метаданные классов.
  • Code Cache — скомпилированный JIT-компилятором код.

Вот простой пример, показывающий, куда попадают данные:

public class MemoryDemo {
    public static void main(String[] args) {
        int number = 42;                    // Примитив — в стеке
        String text = "Hello";              // Ссылка в стеке, объект в куче
        List<String> list = new ArrayList<>(); // Ссылка в стеке, список в куче
        list.add("Java");                   // Строка "Java" — тоже в куче
    }
}

Ключевой момент: все объекты, созданные через new, попадают в heap Java. Именно за этой областью «следит» сборщик мусора в Java.


Почему сборщик мусора — не враг, а друг (если понимать его логику)

Многие разработчики боятся Java GC из-за «Stop-The-World» пауз. Но давайте посмотрим правде в глаза: без автоматического управления памятью Java мы бы писали вручную free() и ловили segfault'ы. Сборщик мусора Java — это не злой демон, а сложный, умный механизм.

Основные принципы работы garbage collection Java:

  • Объект считается «мусором», если на него нет активных ссылок из стека или статических полей.
  • GC не может удалить объект, на который всё ещё есть ссылка (даже если вы о ней забыли).
  • Разные алгоритмы GC имеют разные цели: минимальные паузы, максимальная пропускная способность или экономия памяти.

Вот классический пример утечки памяти, когда объект «забывают» убрать из коллекции:

public class LeakExample {
    private static List<byte[]> cache = new ArrayList<>();

    public static void addToCache() {
        byte[] data = new byte[1024 * 1024]; // 1 MB
        cache.add(data); // Ссылка остаётся — GC не тронет
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            addToCache();
        }
        // Здесь будет OutOfMemoryError, хотя объекты уже не нужны
    }
}

Вывод: память в Java не освободится, пока вы держите ссылку. Понимание этого — первый шаг к написанию эффективного кода.


Виды сборщиков мусора в Java: от Serial до ZGC

Современная Java предлагает несколько реализаций garbage collection Java. Выбор зависит от ваших задач:

1. Serial GC — для маленьких приложений

Однопоточный сборщик, который «замораживает» всё приложение. Используется по умолчанию на клиентских машинах.

2. Parallel GC — для высокой пропускной способности

Многопоточный, старается максимально загрузить процессоры. Хорош для бэкенда, где важна скорость обработки данных.

3. CMS (Concurrent Mark Sweep) — устаревший, но легендарный

Старался минимизировать паузы, но страдал от фрагментации памяти. Удалён в Java 14.

4. G1 GC — современный стандарт

Работает с регионами кучи, предсказуемые паузы. Используется по умолчанию с Java 9. Отлично подходит для настройки JVM под большие heap'ы.

5. ZGC и Shenandoah — для мега-низких задержек

Паузы менее 10 мс даже при терабайтных кучах. Экспериментальные, но уже production-ready.

Как выбрать? Если ваше приложение — это микросервис с кучей 2 ГБ, G1 GC будет оптимальным выбором. Для real-time систем — ZGC.


Как объекты путешествуют по поколениям: молодые и старые

Одна из ключевых идей Java memory model — гипотеза о поколениях: «большинство объектов живут недолго». На этом основана вся архитектура heap Java:

  • Young Generation (Молодое поколение) — здесь создаются новые объекты. Делится на Eden и Survivor spaces (S0, S1).
  • Old Generation (Старое поколение) — сюда перемещаются объекты, пережившие несколько сборок мусора.
  • Metaspace — для метаданных классов (не входит в heap).

Процесс «взросления» объекта выглядит так:

// Объект создаётся в Eden
MyObject obj = new MyObject();

// После Minor GC, если obj выжил, он перемещается в S0 или S1
// После нескольких циклов — в Old Generation
// И только Full GC может удалить его оттуда

Понимание поколений помогает настраивать управление памятью Java. Например, если у вас много долгоживущих объектов, увеличьте Old Generation.


Настройка JVM: практические советы для профи

Теперь, когда мы разобрались с теорией, давайте поговорим о практике. Настройка JVM — это искусство, но есть несколько проверенных правил:

1. Начните с размера кучи

-Xms2g -Xmx2g  // Минимальный и максимальный размер heap — одинаковый

Совет: не ставьте

Блоги

Рекомендаций по книгам