Сборка мусора (Garbage Collection) в Java: от основ до продвинутой настройки

Введение

Сборка мусора (Garbage Collection, GC) — одна из фундаментальных особенностей Java, которая освобождает разработчика от ручного управления памятью. Однако за кажущейся простотой скрывается сложный механизм, от эффективности работы которого напрямую зависит производительность всего приложения. Современные Java-приложения обрабатывают терабайты данных и миллионы транзакций в секунду, и правильная настройка GC становится критически важной для достижения высокой производительности и низкой латентности.


Архитектура памяти JVM: где живут объекты

1.1. Структура кучи (Heap)

Память в JVM разделена на несколько областей, каждая из которых обслуживает определенный тип объектов:

oung Generation (Молодое поколение)
├── Eden Space (Эдем)          - Здесь создаются новые объекты
└── Survivor Spaces (S0 и S1)  - Выжившие после minor GC объекты

Old Generation (Старое поколение) - Долгоживущие объекты

Metaspace (Java 8+) / PermGen (до Java 7) - Метаданные классов

Жизненный цикл объекта:

public void processOrder(OrderRequest request) {
    // 1. Объект создается в Eden Space
    Order order = new Order(request);
    
    // 2. Временные объекты для расчетов
    CalculationContext context = new CalculationContext(order);
    BigDecimal total = calculator.calculateTotal(context);
    
    // 3. После завершения метода:
    // - 'context' становится недостижим → будет удален при следующем minor GC
    // - 'order' может быть промотирован в Old Gen, если сохраняется в кэше
}

1.2. Размеры областей памяти

Оптимальные соотношения размеров областей (для большинства приложений):

  • Young Generation: 1/3 от всей кучи

  • Old Generation: 2/3 от всей кучи

  • Eden : Survivor: 8:1:1 (Eden в 8 раз больше каждого Survivor)

Настройка в JVM аргументах:

# Пример настройки размеров для приложения с 4GB heap
-Xms4096m -Xmx4096m           # Минимальный и максимальный размер кучи
-XX:NewRatio=2                # OldGen в 2 раза больше YoungGen (1:2)
-XX:SurvivorRatio=8           # Eden:S0:S1 = 8:1:1
-XX:MetaspaceSize=256m        # Начальный размер Metaspace
-XX:MaxMetaspaceSize=512m     # Максимальный размер Metaspace

Алгоритмы сборки мусора

2.1. Типы сборщиков и их эволюция

Serial GC (-XX:+UseSerialGC)

# Конфигурация для малых приложений
-XX:+UseSerialGC
-XX:MaxGCPauseMillis=100
  • Принцип работы: Один поток, Stop-The-World паузы

  • Плюсы: Простота, низкий overhead

  • Минусы: Длинные паузы, не подходит для больших heap

  • Использование: CLI утилиты, tiny-микросервисы (< 100MB heap)

Parallel GC / Throughput Collector (-XX:+UseParallelGC)

# Конфигурация для batch-обработки
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=99
  • Принцип работы: Многопоточная сборка в Young и Old Generation

  • Плюсы: Максимальная пропускная способность (throughput)

  • Минусы: Все еще длинные STW паузы

  • Использование: Обработка данных, ETL-процессы, где throughput важнее latency

CMS — Concurrent Mark Sweep (-XX:+UseConcMarkSweepGC)

# Конфигурация для low-latency приложений (устаревший)
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSScavengeBeforeRemark
  • Принцип работы: Большая часть работы выполняется concurrently

  • Плюсы: Короткие STW паузы

  • Минусы: Фрагментация памяти, сложная настройка

  • Статус: Deprecated в Java 9, удален в Java 14

G1 GC — Garbage First (-XX:+UseG1GC)

# Современная конфигурация по умолчанию (Java 9+)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=4m
-XX:InitiatingHeapOccupancyPercent=45
  • Принцип работы: Делит heap на регионы, собирает наиболее заполненные

  • Плюсы: Предсказуемые паузы, хороший баланс throughput/latency

  • Минусы: Высокий memory footprint

  • Использование: Стандарт для большинства приложений

ZGC — Z Garbage Collector (-XX:+UseZGC)

# Конфигурация для ultra-low-latency
-XX:+UseZGC
-XX:MaxGCPauseMillis=10
-XX:ConcGCThreads=4
-Xmx16g
  • Принцип работы: Concurrent, compaction, colored pointers

  • Плюсы: Паузы < 10ms независимо от размера heap

  • Минусы: Экспериментальный (production-ready с Java 15)

  • Использование: Financial trading, real-time системы

Shenandoah GC (-XX:+UseShenandoahGC)

# Альтернатива ZGC от Red Hat
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive
-XX:ShenandoahGuaranteedGCInterval=10000
  • Принцип работы: Concurrent evacuation, forwarding pointers

  • Плюсы: Низкая latency, хорошая производительность

  • Минусы: Не включен по умолчанию в OpenJDK

  • Использование: Аналогично ZGC

Мониторинг и анализ работы GC

3.1. Логирование GC событий

# Подробное логирование для анализа
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime
-Xloggc:/var/log/myapp/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M

Анализ логов:

# Minor GC пример
2024-01-15T10:30:15.123+0300: 45.678: [GC (Allocation Failure) 
[PSYoungGen: 1572864K->174592K(1835008K)] 
2097152K->1038848K(6291456K), 
0.3456789 secs] 
[Times: user=1.23 sys=0.12, real=0.35 secs]

# Параметры:
# - PSYoungGen: Young generation использования до/после (размер)
# - Allocation Failure: Причина GC - нехватка памяти в Eden
# - Times: CPU время vs реальное время

3.2. Инструменты мониторинга

JConsole / VisualVM:

  • Real-time графики использования памяти

  • Статистика GC пауз

  • Мониторинг потоков сборщика

JMX мониторинг:

import javax.management.*;
import java.lang.management.*;

public class GCMonitor {
    public void printGCStats() {
        List<GarbageCollectorMXBean> gcBeans = 
            ManagementFactory.getGarbageCollectorMXBeans();
        
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            System.out.println("GC Name: " + gcBean.getName());
            System.out.println("Collection count: " + gcBean.getCollectionCount());
            System.out.println("Collection time: " + gcBean.getCollectionTime() + "ms");
            System.out.println("Memory pools: " + 
                Arrays.toString(gcBean.getMemoryPoolNames()));
        }
        
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        System.out.println("Heap used: " + heapUsage.getUsed() / 1024 / 1024 + "MB");
        System.out.println("Heap max: " + heapUsage.getMax() / 1024 / 1024 + "MB");
    }
}

Prometheus + Grafana:

# JMX Exporter конфигурация
jmxUrl: service:jmx:rmi:///jndi/rmi://localhost:9091/jmxrmi
lowercaseOutputName: true
lowercaseOutputLabelNames: true
rules:
  - pattern: 'java.lang<type=GarbageCollector, name=(.+)><>(.+)'
    name: jvm_gc_$2
    labels:
      gc: $1

Продвинутая настройка GC

4.1. Тюнинг G1 GC для production

# Оптимизация для веб-приложения с 8GB heap
-XX:+UseG1GC
-Xms8g -Xmx8g                     # Фиксированный размер кучи
-XX:MaxGCPauseMillis=200          # Целевая максимальная пауза
-XX:G1HeapRegionSize=4m           # Размер региона (1-32MB)
-XX:InitiatingHeapOccupancyPercent=45  # Когда начинать mixed GC

# Параметры параллелизма
-XX:ConcGCThreads=4               # Concurrent threads (1/4 от ParallelGCThreads)
-XX:ParallelGCThreads=16          # Parallel threads (обычно = числу ядер)

# Оптимизация смешанных сборок
-XX:G1MixedGCLiveThresholdPercent=85
-XX:G1HeapWastePercent=5
-XX:G1MixedGCCountTarget=8

# Экстренные параметры
-XX:+UnlockExperimentalVMOptions
-XX:G1MaxNewSizePercent=60        # Макс размер Young Gen
-XX:G1NewSizePercent=5            # Мин размер Young Gen
-XX:+G1UseAdaptiveConcRefinement

4.2. Настройка для конкретных сценариев

Веб-приложение (Spring Boot, высокий RPS):

# Цель: баланс между throughput и latency
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=35  # Ранний старт GC
-XX:G1ReservePercent=15                # Резерв для promotion failure
-XX:+ParallelRefProcEnabled            # Параллельная обработка ссылок

Микросервис в контейнере (Kubernetes):

# Цель: предсказуемое потребление памяти
-XX:+UseContainerSupport              # Учет ограничений контейнера
-XX:MaxRAMPercentage=75.0             # Использовать 75% от доступной памяти
-XX:InitialRAMPercentage=75.0
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:NativeMemoryTracking=summary      # Мониторинг native memory
-XX:+PrintContainerInfo

Data processing (Apache Spark, Flink):

# Цель: максимальный throughput
-XX:+UseParallelGC
-Xmx32g -Xms32g
-XX:MaxGCPauseMillis=500              # Допустимы более длинные паузы
-XX:GCTimeRatio=19                    # 95% времени на приложение (19:1)
-XX:ParallelGCThreads=32
-XX:+UseAdaptiveSizePolicy            # Автонастройка размеров поколений
-XX:+AlwaysPreTouch                   # Преаллокация всей памяти

Real-time система (финансовые транзакции):

# Цель: минимальная и предсказуемая latency
-XX:+UseZGC
-Xmx16g -Xms16g
-XX:MaxGCPauseMillis=5                # Ультра-низкие паузы
-XX:ConcGCThreads=8
-XX:+UseLargePages                    # Performance optimization
-XX:+UnlockDiagnosticVMOptions
-XX:+ZStatistics                      # Детальная статистика

4.3. Работа с большими объектами

Humongous Objects в G1:

// Объекты > 50% региона считаются humongous
byte[] hugeArray = new byte[10 * 1024 * 1024]; // 10MB

// Проблемы:
// 1. Прямое размещение в Old Gen
// 2. Могут вызывать premature promotion
// 3. Сложности с compaction

// Решение:
-XX:G1HeapRegionSize=8m              # Увеличить размер региона
-XX:G1MixedGCLiveThresholdPercent=90  # Более агрессивная очистка

Оптимизация через object pooling:

public class BufferPool {
    private static final int BUFFER_SIZE = 8192;
    private static final Queue<byte[]> pool = 
        new ConcurrentLinkedQueue<>();
    
    public static byte[] getBuffer() {
        byte[] buffer = pool.poll();
        return buffer != null ? buffer : new byte[BUFFER_SIZE];
    }
    
    public static void returnBuffer(byte[] buffer) {
        if (buffer != null && buffer.length == BUFFER_SIZE) {
            Arrays.fill(buffer, (byte) 0); // Очистка
            pool.offer(buffer);
        }
    }
}

Заключение

Сборка мусора в Java прошла долгий путь от простого stop-the-world алгоритма до сложных concurrent коллекторов, способных поддерживать паузы менее 10 миллисекунд на терабайтных heap’ах. Ключевой вывод для разработчика: не существует универсальных настроек GC — каждый приложение требует индивидуального подхода, основанного на его workload patterns.

Главные рекомендации:

  1. Начинайте с дефолтов — G1 GC в Java 11+ уже хорошо настроен для большинства сценариев

  2. Измеряйте перед оптимизацией — используйте JFR, GC лог анализ, APM инструменты

  3. Понимайте свой workload — throughput-oriented vs latency-sensitive приложения требуют разных подходов

  4. Тестируйте под нагрузкой — симуляция production трафика обязательна для настройки GC