Published on

JVM, Memory Management и Performance

Authors
  1. Объясните полный жизненный цикл объекта в куче (Heap). От создания до сборки мусора, включая поколения (Young Gen, Old Gen), Eden, S0, S1.
  2. Что такое Garbage Collection (GC)? Объясните основные алгоритмы (Mark-Sweep, Mark-Compact, Copying) и их trade-offs.
  3. Опишите различия между Serial, Parallel, CMS, G1 и ZGC сборщиками мусора. В каких сценариях какой предпочтительнее?
  4. Что такое Stop-The-World (STW) паузы? Как разные GC влияют на их длительность и частоту?
  5. Объясните, что такое "memory leak" в Java. Приведите конкретные примеры из практики (например, в статических коллекциях, кэшах, незакрытых ресурсах).
  6. Что такое Metaspace (Java 8+) и чем она отличается от PermGen? Что вызывает OutOfMemoryError: Metaspace?
  7. Объясните String Pool (String Table). Как работает метод intern() и когда его использование оправдано?
  8. Что такое Escape Analysis и как она помогает в оптимизации? (Связь с Stack Allocation и Scalar Replacement).
  9. Опишите структуру памяти Java-потока (Stack Memory). Что хранится во фрейме метода (local variables, operand stack, reference to runtime constant pool)?
  10. Что такое JIT-компиляция (C1, C2/C1 и C2 (Tiered Compilation))? Что такое "профилирование" кода и деоптимизация?
  11. Объясните принцип работы volatile переменной. Что такое "happens-before" и как это обеспечивает видимость изменений между потоками?
  12. Что такое false sharing (ложное разделение) и как его избежать? (Например, с помощью @Contended).

Ответы на вопросы:

*в этой статье есть упрощения

1. Жизненный цикл объекта в куче: от аллокации до реинкарнации

Создание (Allocation):

  1. Подавляющее большинство объектов аллоцируются в Eden Space (Young Generation). Аллокация происходит через механизм Pointer Bump (TLAB — Thread-Local Allocation Buffer), что сводит операцию к инкременту указателя — O(1), почти не требующему синхронизации.
  2. Крупные объекты (порог зависит от JVM, часто > 512Кб-1Мб) напрямую попадают в Old Generation (Humongous Region в G1), минуя Young Gen, чтобы избежать дорогостоящего копирования.

Ранняя жизнь в Young Generation (Короткоживущие объекты):

  • Eden: При заполнении Eden инициируется Minor GC. Minor GC— это быстрая, частичная очистка оперативной памяти в Java, которая затрагивает только область, называемую Young Generation (Молодое поколение).

  • Алгоритм копирования (Copying): Живые объекты (достижимые от GC Roots) копируются из Eden и одного из Survivor Spaces (S0 или S1) во второй Survivor Space.

  • Survivor Spaces (S0/S1, или From/To): Два идентичных по размеру пространства, всегда одно пустое. (Если бы оба Survivor содержали данные: Некуда было бы копировать новые живые объекты из Eden) После каждого Minor GC живые объекты копируются между ними, и их возраст (age) инкрементируется. Это пространство отсеивает короткоживущие объекты с минимальными издержками.

  • Промоушен (Promotion): При достижении порога возраста (MaxTenuringThreshold, обычно 15) объект считается долгоживущим и перемещается (промотируется) в Old Generation. (упрощение)

Зрелость в Old Generation (Долгоживущие объекты):

  • Объекты переживают длительный срок.
  • Заполнение Old Gen (или достижение определенного порога, InitiatingHeapOccupancyPercent) вызывает Major GC (или Full GC, в зависимости от сборщика), который работает со всей кучей.
  • Алгоритмы в Old Gen более сложные: Mark-Sweep-Compact (Serial, Parallel), Concurrent Mark-Sweep (CMS), или смешанные, как в G1/ZGC/Shenandoah.

Смерть и рециклирование (Garbage Collection):

  • Объект становится мусором, когда нет ни одной ссылки от живого объекта (GC Root) по любому пути достижимости.
  • GC Roots: Статические переменные, активные Stack Frames, JNI References, загруженные системные классы.
  • Память освобождается сборщиком. В Eden/Survivor — путем копирования живых объектов (мертвые игнорируются). В Old Gen — путем "заметания" (sweep) и последующего уплотнения (compact) для борьбы с фрагментацией.

2. Garbage Collection: Основные алгоритмы и компромиссы

  • Garbage Collection — автоматизированная система управления динамической памятью, освобождающая объекты, недостижимые для исполняемой программы.

Алгоритмы:

  1. Mark-Sweep (Пометка-Очистка):

    • Фаза 1 (Mark): Обход графа достижимости от GC Roots. Помечаются живые объекты.
    • Фаза 2 (Sweep): Линейный проход по всей памяти. Непомеченные (мертвые) блоки помечаются как свободные.
    • Trade-offs: Производит фрагментацию. Низкие накладные расходы, но приводит к "дырчатой" куче, что деградирует производительность аллокации и может вызвать OOM при нехватке непрерывного пространства.
  2. Copying (Копирование):

    • Разделяет память на два полупространства (From и To).
    • Живые объекты копируются из From в To. После копирования все пространство From считается свободным.
    • Trade-offs: Требует в 2 раза больше памяти (половина всегда пуста). Не фрагментирует память. Крайне эффективен, если большинство объектов умирают молодыми. Используется только в Young Generation.
  3. Mark-Compact (Пометка-Уплотнение):

    • Фаза 1 (Mark): Аналогично Mark-Sweep.
    • Фаза 2 (Compact): Живые объекты перемещаются в начало региона, образуя непрерывный блок памяти. Обновляются все ссылки на перемещенные объекты.
    • Trade-offs: Устраняет фрагментацию. Наиболее дорогая операция из-за затрат на перемещение и обновление ссылок. Используется преимущественно в Old Generation.

Эволюционный вывод: Young Gen использует Copying (высокая смертность, эффективность). Old Gen использует гибриды Mark-Sweep/Compact (низкая смертность, борьба с фрагментацией). Современные GC (G1, ZGC) разбивают кучу на регионы, применяя алгоритмы точечно.


3. Сборщики мусора: Стратегический выбор

  • Serial GC (-XX:+UseSerialGC): Однопоточный, для Mark, Sweep, Compact. Только STW. Сценарий: Однопоточные приложения, микроконтроллеры, окружения с минимальными ресурсами.
  • Parallel GC (Throughput Collector) (-XX:+UseParallelGC): Многопоточные версии Serial для Young и Old Gen. Максимизирует пропускную способность (throughput) за счет более агрессивного использования CPU и более длинных STW пауз. Сценарий: Пакетная обработка, вычисления, где допустимы паузы в сотни миллисекунд-секунды.
  • CMS – Concurrent Mark Sweep (-XX:+UseConcMarkSweepGC): Уменьшает длительность STW пауз за счет параллельной работы сборщика и приложения.
    • Фазы: Initial Mark (STW, быстрая), Concurrent Mark, Concurrent Preclean, Remark (STW), Concurrent Sweep.
    • Trade-offs: Не выполняет compaction по умолчанию → фрагментация, возможен Concurrent Mode Failure (вынужденный Full GC). Высокое потребление CPU в фоновом режиме.
  • G1 – Garbage First (-XX:+UseG1GC, дефолт с Java 9-11): Региональный (-XX:G1HeapRegionSize), прогнозирующий.
    • Делит кучу на ~2000 регионов. Собирает регионы с наибольшим количеством мусора (Garbage First). Имеет мягкие real-time цели (-XX:MaxGCPauseMillis).
    • Сценарий: Универсальный баланс между throughput и latency. Основной выбор для большинства приложений с кучей >4-6Гб.
  • ZGC (-XX:+UseZGC) и Shenandoah (-XX:+UseShenandoahGC): Низколатентные (sub-millisecond цели) сборщики.
    • Ключевая черта: Практически все фазы, включая перемещение объектов, выполняются конкурентно с приложением.
    • Используют окрашивание указателей (colored pointers) и барьеры на чтение/запись (load barriers).
    • Сценарий: Приложения, критичные к задержкам: финансовые транзакции, высоконагруженные веб-сервисы, большие heaps (терабайты).

Стратегия выбора: Чем меньше допустимая задержка (latency), тем более продвинутый и конкурентный сборщик требуется. Throughput -> Latency градиент: Parallel -> G1 -> ZGC/Shenandoah.


4. Stop-The-World (STW): Анатомия заморозки

  • STW — фаза, когда все потоки приложения (application threads) приостанавливаются для выполнения операции GC, безопасной по отношению к изменяющемуся графу объектов.
  • Причины: Корневая сканировка (Root Scanning), фаза Remark в CMS/G1 (учет изменений за время конкурентной маркировки), эвакуация (Evacuation) и уплотнение (Compaction) в не-конкурентных фазах.
  • Влияние GC:
    • Serial/Parallel: Доминирующие, длительные STW-фазы. Паузы растут с размером кучи.
    • CMS: Значительно сокращает STW (Initial Mark, Remark), но оставляет риск Concurrent Mode Failure (длительный STW).
    • G1: Прогнозируемые, управляемые паузы (MaxGCPauseMillis). STW ограничены эвакуацией выбранного набора регионов.
    • ZGC/Shenandoah: STW сведены к микросекундной корневой сканировке (Root Scanning). Большая часть работы — конкурентна.

5. Memory Leak в Java: Систематический сбой

  • Утечка памяти — ситуация, когда объекты больше не используются приложением, но не могут быть собраны GC из-за оставшихся некорректных ссылок, хранящихся в живых структурах данных.
  • Это не ошибка JVM, а логическая ошибка в коде.

Канонические примеры:

  1. Статические коллекции (Классика):
    public class LeakyClass {
        private static final List<byte[]> STATIC_CACHE = new ArrayList<>();
        public void processData(byte[] data) {
            STATIC_CACHE.add(data); // Объект data вечно достижим через статическое поле
        }
    }
    
  2. Неконтролируемые кэши (Guava Cache, Caffeine без политики вытеснения):
    Cache<Key, Value> cache = Caffeine.newBuilder().build(); // Нет expireAfterWrite или maximumSize
    // Кэш растет бесконечно.
    
  3. Незакрытые ресурсы (InputStream, Connection, Session): Ресурсы часто держат ссылки на внутренние буферы или объекты в native memory. Решение: try-with-resources.
  4. Слушатели событий (Listeners) и внутренние классы: Неотписка от слушателя, сохраненного в глобальном контексте, держит ссылку на внешний класс.
  5. ThreadLocal без очистки (особенно в пулах потоков): Значение в ThreadLocal живет, пока жив поток. В web-приложениях поток возвращается в пул и живет годами.
    private static final ThreadLocal<HeavyContext> threadLocal = new ThreadLocal<>();
    // После использования необходимо: threadLocal.remove();
    

Диагностика: Мониторинг Old Gen (постоянный рост), анализ heap dump (jmap -dump, MAT, VisualVM), поиск java.lang.Object[]. с наибольшим retained size.


6. Metaspace vs PermGen: Эволюция метаданных

PermGen (до Java 7) — фиксированный сегмент кучи для метаданных классов, вызывавший частые OutOfMemoryError и требовавший ручной настройки размера.

Metaspace (с Java 8) — динамическая область в нативной памяти, автоматически управляемая ОС, что устранило проблемы PermGen и позволило эффективно загружать и выгружать классы.

  • PermGen (≤ Java 7): Фиксированный размер (-XX:MaxPermSize). Хранил метаданные классов, interned строки, статические члены. Частая причина OutOfMemoryError: PermGen space.
  • Metaspace (Java 8+): Нативная память (не часть Java Heap).
    • Управляется ОС, по умолчанию неограничен (ограничено физической памятью/swap).
    • Автоматический рост и очистка. Класс-лоадеры и их загруженные классы собираются GC.
    • Разделена: Klass Metaspace (несбрасываемые метаданные), NoKlass Metaspace для прочего.
  • OutOfMemoryError: Metaspace возникает при:
    1. Достижении лимита (-XX:MaxMetaspaceSize).
    2. Утечке метаданных (ClassLoader Leak): Частая причина — контейнеры (Tomcat, OSGi), где перезагружаются приложения, но старый ClassLoader удерживается (например, через поток или статическую ссылку), не позволяя выгрузить его классы.

7. String Pool (String Table): Механизм дедупликации

  • String Pool — хэш-таблица (Hashtable) в heap (ранее в PermGen), хранящая канонические (interned) экземпляры String.
  • Правила:
    1. Строковые литералы ("text") добавляются в Pool на этапе загрузки класса.
    2. String.intern(): Позволяет добавить строку, созданную в runtime, в Pool. Возвращает каноническое представление.
      • Если строка уже есть в Pool — возвращает ссылку на нее.
      • Если нет — добавляет текущий объект в Pool и возвращает его же.
  • Когда использовать intern():
    • Почти никогда в типовом прикладном коде.
    • Оправданно: При обработке огромных объемов данных с высокой степенью дублирования строк (парсинг CSV, теги, enum-подобные значения), когда требуется:
      • Существенная экономия памяти (одна строка на множество идентичных значений).
      • Ускорение сравнения через == (замена .equals()).
    • Опасность: Неконтролируемое использование приводит к росту Pool, который никогда не очищается (до Java 7). С Java 7+ interned строки лежат в heap и могут собираться GC, если ClassLoader выгружен.

8. Escape Analysis: Компиляторная магия оптимизации

  • Escape Analysis (EA) — анализ JIT-компилятора (C2), определяющий диапазон видимости (scope) создаваемого объекта.
    • NoEscape: Объект не покидает пределы метода и/или потока.
    • ArgEscape: Объект передается в другой метод, но не "сбегает" из потока.
    • GlobalEscape: Объект публикуется (сохраняется в статическое поле, передается в другой поток).
  • На основе EA JIT применяет оптимизации:
    1. Scalar Replacement (Разложение на скаляры): Если объект NoEscape, JIT не аллоцирует его в куче. Вместо этого его поля преобразуются в локальные переменные метода (примитивы/ссылки) на стеке. Идеальная оптимизация: нулевые аллокационные издержки, нулевые накладные расходы на GC.
      // До оптимизации
      Point p = new Point(x, y);
      return p.x + p.y;
      // После Scalar Replacement
      int p_x = x, p_y = y;
      return p_x + p_y; // Объект Point не создается.
      
    2. Stack Allocation: Частный случай Scalar Replacement. Теоретическое размещение на стеке, но в HotSpot реализовано именно как разложение.
    3. Lock Elision (Устранение блокировок): Если монитор объекта NoEscape (например, synchronized-блок на локальном объекте), блокировка удаляется, так как она не может быть contended в другом потоке.

Активация: Включена по умолчанию (-XX:+DoEscapeAnalysis). Эффективна для короткоживущих, локальных объектов (DTO, итераторы, билдеры).


9. Память потока (Stack Memory): Архитектура фрейма

Каждый поток JVM имеет приватный стек, создаваемый при его запуске. Стек состоит из фреймов (stack frames), помещаемых (push) при вызове метода и вынимаемых (pop) при его завершении (нормальном или исключении).

Структура фрейма метода:

  1. Local Variable Array (LVA): Массив переменных метода, индексируемый с 0.
    • this (для не-статических методов) хранится в LVA[0].
    • Параметры метода — в LVA[1], LVA[2], ...
    • Локальные переменные — в последующих слотах.
    • Каждый слот 32 бита (int, float, reference). long/double занимают 2 слота.
  2. Operand Stack (OS): Рабочая область для вычислений (по типу стека архитектуры). Инструкции bytecode (iload, iadd, invokevirtual) оперируют с этим стеком (push/pop значений).
    int a = 5; int b = 3; int c = a + b;
    // Bytecode:
    iconst_5 // push 5 -> OS
    istore_1 // pop OS -> LVA[1] (a)
    iconst_3 // push 3 -> OS
    istore_2 // pop OS -> LVA[2] (b)
    iload_1  // push LVA[1] (a) -> OS
    iload_2  // push LVA[2] (b) -> OS
    iadd     // pop 2 values, add, push result -> OS
    istore_3 // pop OS -> LVA[3] (c)
    
  3. Reference to Runtime Constant Pool (RCP): Указатель на Constant Pool класса, необходимый для разрешения символьных ссылок (имена методов, классов, константы) во время выполнения.

Размер: Задается параметром -Xss (по умолчанию ~1Мб). Переполнение → StackOverflowError. Динамическое расширение → OutOfMemoryError.


10. JIT-компиляция: C1, C2 и адаптивная оптимизация

  • JIT (Just-In-Time) — компиляция "горячего" байткода в нативный машинный код во время выполнения.

  • Уровни компиляции в HotSpot (Tiered Compilation, -XX:+TieredCompilation):

    • Интерпретатор: Выполняет байткод. Нулевые накладные расходы на старт, но низкая скорость.
    • C1 (Client Compiler): Быстрая, легковесная компиляция. Применяет базовые оптимизации (inlining, простой анализ потока данных). Цель — быстро получить работающий нативный код.
    • C2 (Server Compiler): Агрессивный, тяжелый оптимизирующий компилятор. Использует сложный статический анализ (EA, скалярная замена, размотка циклов, макро- и микрослияние, оптимизации памяти и барьеров). Компилирует самые горячие методы.
  • Профилирование (Profiling): JVM собирает данные о работе кода в рантайме:

    • Счётчики вызовов методов.
    • Ветвление (branch): Какая ветка if выполняется чаще.
    • Типы (Type Profile): Какие конкретные классы приходят в полиморфный вызов (invokevirtual). Это позволяет сделать девиртуализацию — заменить виртуальный вызов на прямой, а затем и инлайнинг.
  • Деоптимизация (Deoptimization): Обратный процесс. Если предположения оптимизатора нарушаются (например, пришел новый тип, неучтенный в профиле), JVM откатывает скомпилированный нативный код обратно к интерпретируемому байткоду.

    • Триггеры: "Устаревший" профиль (class loading, новые полиморфные типы), отладочные точки (breakpoint), сброс зависимостей. (упрощение)

Цикл: Интерпретатор → профилирование → C1 → профилирование → C2 → (деоптимизация при необходимости). Это Adaptive Optimization.


11. volatile: Гарантии видимости и упорядочения

  • volatile — модификатор переменной, обеспечивающий гарантии видимости и упорядочения на уровне памяти, без атомарности составных операций (i++).

  • Семантика:

    1. Видимость (Visibility): Запись в volatile-переменную одним потоком гарантированно становится видна всем последующим чтениям этой переменной из других потоков.
    2. Запрет переупорядочения (Ordering): JVM и процессор не могут переставить операции чтения/записи volatile переменной с другими операциями памяти таким образом, что это нарушило бы правило happens-before.
  • Happens-Before (Произходит-До): Формальная модель памяти Java, определяющая гарантии видимости изменений между потоками.

    • Правило для volatile (JLS 17.4.5): Запись в volatile-поле happens-before каждое последующее чтение того же поля.
    • Следствие (Transitivity): Если поток A пишет в volatile V, а затем поток B читает V, то все изменения памяти, сделанные потоком A до записи в V, становятся видимыми потоку B после чтения V.
      // Thread 1
      sharedNonVolatileData = ...; // (1)
      volatileFlag = true;          // (2) volatile write
      // Thread 2
      if (volatileFlag) {           // (3) volatile read (увидит true)
          // Здесь гарантированно видно значение sharedNonVolatileData из (1)
          use(sharedNonVolatileData);
      }
      
  • Реализация: На уровне процессора это обычно реализуется через барьеры памяти (Memory Barrier или Fence). Запись volatile включает StoreStore + StoreLoad барьеры. Чтение — LoadLoad + LoadStore.

Использование: Для флагов завершения, публикации результатов (safe publication), в паттернах типа double-checked lockingvolatile).


12. False Sharing (Ложное разделение): Скрытый враг производительности

  • False Sharing — деградация производительности в многопоточных системах, возникающая, когда два независимых частомодифицируемых поля (M1 и M2), принадлежащие разным объектам (или разным элементам массива), попадают в одну и ту же строку кэша (cache line, обычно 64 байта) процессора.
  • Механизм: Процессоры поддерживают когерентность кэшей по протоколу MESI. Если поток на ядре 1 изменяет M1, вся строка кэша помечается как "модифицированная" (Modified), что инвалидирует эту же строку кэша на ядре 2, даже если там лежит только M2. Ядро 2 при доступе к M2 вынуждено перечитывать строку из памяти, хотя само значение M2 не изменилось. Это вызывает каскадную инвалидацию и "гонку" за строкой кэша.
  • Последствие: Кажущиеся независимыми операции начинают синхронно конкурировать, вызывая резкое падение масштабируемости.

Решение — выравнивание (Padding, @Contended):

  1. Классический паддинг (до Java 8): Добавление "пустых" полей для разнесения критичных полей по разным строкам кэша.

    class Counter {
        volatile long count1;
        private long p1, p2, p3, p4, p5, p6, p7; // Паддинг ~56 байт
        volatile long count2;
    }
    
  2. @sun.misc.Contended (Java 8+): Аннотация, инструктирующая JVM автоматически добавить паддинг вокруг поля или всего класса.

    import jdk.internal.vm.annotation.Contended;
    public class StripedCounter {
        @Contended // JVM добавит паддинг (~128 байт) вокруг каждого поля
        volatile long cell1;
        @Contended
        volatile long cell2;
    }
    
    • Требует -XX:-RestrictContended для использования вне java.base.
    • Широко используется во внутренностях JDK (LongAdder, Thread, ForkJoinPool).
  • Альтернативы: Проектирование структур данных так, чтобы потоки работали с независимыми областями памяти (локальные переменные, ThreadLocal), или использование поточных структур типа LongAdder.

Диагностика: Профилировщики (VTune, perf) могут отслеживать события RESOURCE_STALLS.L1D_MISS_CYCLES или MEM_LOAD_RETIRED.L2_MISS. В Java — эмпирически, по падению производительности при добавлении, казалось бы, независимых операций.

Еще раз, и возможно чуть нагляднее ->

ЧАСТЬ 1: АРХИТЕКТУРА ПАМЯТИ JVM - МАКРОУРОВЕНЬ

Heap: Доминирующая структура в JVM

Физическая организация (64-bit HotSpot JVM):

┌─────────────────────────────────────────────────────────────┐
HEAP (Max: 32/64 TB)├──────────────┬─────────────────┬────────────────────────────┤
YOUNG GEN   │                 │        OLD GEN  (1-3 регионов)       (2/3 кучи)├──────────────┼─────────────────┼────────────────────────────┤
EDENSURVIVOR S0   │                            │
   (80% YG)SURVIVOR S1   │       Континнуальные       │
   (по 10% YG)   │       объекты, пережившие  │
│              │                 │       много GC├──────────────┴─────────────────┴────────────────────────────┤
METASPACE  (Class metadata, методы, константы, аннотации)└─────────────────────────────────────────────────────────────┘

Количественные параметры (по умолчанию):

  • -Xms / -Xmx: Начальный/Максимальный размер кучи
  • -XX:NewRatio=2: OldGen:YoungGen = 2:1
  • -XX:SurvivorRatio=8: Eden:Survivor = 8:1 (каждый Survivor)
  • -XX:MaxTenuringThreshold=15: Максимальный возраст для промоута

Жизненный цикл объекта: детальная хронология

Фаза 1: Аллокация в Eden

public class AllocationPatterns {
    // TLAB (Thread-Local Allocation Buffer) - ключевая оптимизация
    static void demonstrateTLAB() {
        // При создании объекта:
        // 1. Проверка: достаточно ли места в текущем TLAB?
        // 2. Если да: pointer bump allocation (pointer += size)
        // 3. Если нет: запрос нового TLAB у Eden

        // Размер TLAB настраивается:
        // -XX:TLABSize=512k (размер)
        // -XX:+ResizeTLAB (автоматический ресайз)

        for (int i = 0; i < 100_000; i++) {
            // 99% объектов аллоцируются здесь
            Object obj = new Object(); // ~12 bytes + overhead
        }
    }
}

Механика аллокации:

  1. Pointer Bump в TLAB: current_ptr += object_size
  2. Zeroing memory: JVM обнуляет память для безопасности
  3. Установка Mark Word: mark = hash/age/lock_bits
  4. Установка Klass Pointer: ссылка на Class объекта

Стоимость: 10-20 циклов CPU для малого объекта


Фаза 2: Первый Minor GC

Триггер: Eden заполнена на 80-90% (адаптивно)

Алгоритм Copying Collector:

// Псевдокод HotSpot (Young GC)
void youngGC() {
    // 1. Stop-The-World: приостановка всех потоков
    stop_all_threads();

    // 2. Root scanning (очень быстрая)
    scan_roots();

    // 3. Copy живых из Eden и From-Survivor в To-Survivor
    for (Object obj : Eden + From_Survivor) {
        if (is_alive(obj)) {
            new_location = copy_to(obj, To_Survivor);
            forward_pointer(obj, new_location); // Для обновления ссылок
        }
    }

    // 4. Swap Survivor spaces
    swap_survivors();

    // 5. Возраст объектов в Survivor
    for (Object obj in To_Survivor) {
        obj.age++;
        if (obj.age >= threshold) {
            promote_to_old_gen(obj);
        }
    }

    // 6. Resume
    resume_all_threads();
}

Критические детали:

  • Card Table: Bitmap для отслеживания ссылок из OldGen в YoungGen
  • Remembered Sets: В G1/ZGC для отслеживания межрегиональных ссылок

Фаза 3: Промоушн в Old Generation

Условия промоута:

  1. Возрастной порог: age >= MaxTenuringThreshold (обычно 15)
  2. Размер Survivor: Если Survivor переполнен, самые старые объекты промотятся
  3. Крупные объекты: > -XX:PretenureSizeThreshold (обычно 1MB) сразу в OldGen
// Пример: создание долгоживущих объектов
static void createLongLivedObjects() {
    List<byte[]> longLived = new ArrayList<>();

    // Эти объекты переживут несколько Minor GC
    for (int i = 0; i < 100; i++) {
        // 100KB - достаточно для промоута после нескольких GC
        byte[] data = new byte[102400];
        longLived.add(data);

        // Создаем мусор для провокации GC
        for (int j = 0; j < 1000; j++) {
            byte[] garbage = new byte[1024]; // Будет собран
        }
    }
}

Модели сборщиков мусора: эволюция алгоритмов

1. Serial Collector (Mark-Sweep-Compact)

Алгоритм:
  1. Mark: Обход графа достижимости от GC Roots
  2. Sweep: Освобождение непомеченных областей
  3. Compact: Дефрагментация (опционально)

Особенности:
  - Single-threaded (STW на всё время)
  - Простой, низкие накладные расходы
  - Идеально для embedded и клиентских приложений

2. Parallel / Throughput Collector

Алгоритм:
  - Многопоточные версии Serial для всех фаз
  - Цель: максимизировать throughput (приложение/GC)

Конфигурация:
  -XX:+UseParallelGC
  -XX:ParallelGCThreads=(CPU cores)
  -XX:MaxGCPauseMillis=200 (цель)
  -XX:GCTimeRatio=99 (99% времени на приложение)

Использование: batch processing, ETL, научные вычисления

3. CMS - Concurrent Mark Sweep (депрекирован)

// Фазы CMS:
1. Initial Mark (STW)      // Быстрая, только direct roots
2. Concurrent Mark         // Параллельно с приложением
3. Remark (STW)           // Учет изменений за время concurrent mark
4. Concurrent Sweep       // Очистка

Проблемы:
  - Фрагментация (нет compaction)
  - Concurrent Mode Failure при быстром заполнении
  - Высокое CPU использование в concurrent фазах

4. G1 - Garbage First (дефолт с Java 9)

Архитектура:
  - Куча делится на ~2000 регионов (1-32MB)
  - Молодое поколение = набор регионов (не фиксировано)
  - Humongous регионы для объектов >50% региона

Алгоритм:
  1. Concurrent marking (как CMS)
  2. Evacuation: копирование живых объектов из "garbage first" регионов
  3. Compaction on-the-fly

Конфигурация:
  -XX:+UseG1GC
  -XX:G1HeapRegionSize={1,2,4,8,16,32}M
  -XX:MaxGCPauseMillis=200
  -XX:InitiatingHeapOccupancyPercent=45

5. ZGC / Shenandoah (Low-Latency)

Инновации:
  - Load barriers вместо write barriers
  - Colored pointers (metadata в указателях)
  - Region-based как G1, но все фазы concurrent

ZGC структура указателя:
  ┌─────────┬──────┬──────┬──────────────────────┐
42 bits │ 4b   │ 4b   │ 14b                  │
Address0000MarkUnused  └─────────┴──────┴──────┴──────────────────────┘

Преимущества:
  - STW < 1ms независимо от размера кучи
  - Поддержка терабайтных куч

ЧАСТЬ 2: STOP-THE-WORLD - АРХИТЕКТУРНЫЙ ВЗГЛЯД

Анатомия паузы JVM

// HotSpot VM операция safepoint
void SafepointSynchronize::begin() {
    // 1. Установка safepoint флага
    _state = _synchronizing;

    // 2. Остановка всех потоков в safe-точках
    for (JavaThread* thread = Threads::first(); thread; thread = thread->next()) {
        thread->safepoint_state()->examine_state_of_thread();

        // Поток должен остановиться в одном из:
        // - Между байткод инструкциями (в interpreted)
        // - В safepoint polling page (в compiled code)
        // - Блокированным в native code
    }

    // 3. Все потоки остановлены
    _state = _synchronized;

    // 4. Выполнение операции (GC, deopt, etc.)
    do_operation();

    // 5. Возобновление
    _state = _not_synchronized;
}

Safepoint Polling в скомпилированном коде

; x86_64 сгенерированный код JIT
compiled_method:
    ; Пролог
    push   rbp
    mov    rbp, rsp

    ; Тело метода
    mov    rax, [rsi+0x10]  ; Загрузка поля
    add    rax, 0x1
    mov    [rsi+0x10], rax  ; Сохранение

    ; Safepoint poll (каждые ~1000 инструкций)
    test   byte ptr [rip+safepoint_page], 0xff
    jnz    safepoint_handler  ; Переход если safepoint

    ; Продолжение
    ret

safepoint_page:  ; Страница памяти, меняемая при safepoint
    .byte 0

ЧАСТЬ 3: MEMORY LEAK - СИСТЕМНЫЙ АНАЛИЗ

Типология утечек памяти

1. Классическая утечка через статику

public class ClassicLeak {
    // Глобальный кэш без ограничений
    private static final Map<Key, Value> CACHE = new HashMap<>();

    // Утечка: объекты никогда не удаляются
    public void processRequest(Request req) {
        Key key = extractKey(req);
        Value val = computeExpensiveValue(req);
        CACHE.put(key, val);  // Навсегда в памяти
    }

    // Решение 1: WeakHashMap
    private static final Map<Key, Value> WEAK_CACHE =
        Collections.synchronizedMap(new WeakHashMap<>());

    // Решение 2: Guava Cache с политиками
    private static final Cache<Key, Value> GUAVA_CACHE =
        CacheBuilder.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .weakKeys()
            .build();
}

2. ThreadLocal в пуле потоков

public class ThreadLocalLeak {
    private static final ThreadLocal<ByteBuffer> BUFFER_HOLDER =
        new ThreadLocal<ByteBuffer>() {
            @Override
            protected ByteBuffer initialValue() {
                return ByteBuffer.allocateDirect(1024 * 1024); // 1MB direct buffer
            }
        };

    // В web-приложении (Tomcat):
    // Поток возвращается в пул после запроса
    // ThreadLocal не очищается автоматически!
    // Память накапливается: pool_size * buffer_size

    public void handleRequest(HttpServletRequest req) {
        ByteBuffer buffer = BUFFER_HOLDER.get();
        // использование...
        // ЗАБЫВАЕМ: BUFFER_HOLDER.remove();
    }
}

3. Некорректные слушатели событий

public class ListenerLeak {
    private final List<EventListener> listeners = new CopyOnWriteArrayList<>();

    public void registerListener(EventListener listener) {
        listeners.add(listener);
    }

    // НЕТ МЕТОДА unregisterListener!
    // Слушатель держит ссылку на внешний объект
    // → утечка всей цепочки ссылок
}

4. JNI/Off-Heap утечки

public class NativeMemoryLeak {
    static {
        System.loadLibrary("native");
    }

    private native long allocateNativeMemory(int size);
    private native void freeNativeMemory(long pointer);

    public void leak() {
        long ptr = allocateNativeMemory(1024 * 1024); // 1MB native
        // Забываем вызвать freeNativeMemory(ptr)
        // → утечка в native heap (не видна в Java heap dump!)
    }
}

Диагностика утечек:

# 1. Мониторинг в реальном времени
jstat -gc <pid> 1s  # Проверка роста OldGen после Full GC

# 2. Снятие heap dump (продакшен с осторожностью!)
jmap -dump:live,format=b,file=heap.hprof <pid>

# 3. Анализ в Eclipse MAT
#    Ключевые запросы:
#    - "Leak Suspects Report"
#    - "Top Consumers"
#    - "Histogram grouped by class"
#    - "Path to GC Roots"

# 4. Анализ в командной строке
jmap -histo:live <pid> | head -20  # Самые большие классы

# 5. JFR (Java Flight Recorder) для динамического анализа
jcmd <pid> JFR.start duration=60s filename=leak.jfr

ЧАСТЬ 4: METASPACE - МЕТАДАННЫЕ КЛАССОВ

Эволюция от PermGen к Metaspace

PermGen (≤ Java 7):

┌─────────────────────────────────┐
PERMGEN  (Fixed size, часть Heap)├─────────────────────────────────┤
│ • Class metadata                │
│ • Bytecode│ • Runtime constant pool         │
│ • String intern table           │
│ • JIT code cache (частично)└─────────────────────────────────┘
Проблемы: OOM, ручная настройка размера, GC неэффективен

Metaspace (Java 8+):

┌─────────────────────────────────┐
NATIVE MEMORY  (Не Heap, управляется ОС)├─────────────────────────────────┤
METASPACE│  ┌─────────────────────────┐    │
│  │  Non-Class Metaspace    │    │
│  │  ┌───────────────────┐  │    │
│  │  │ Chunk (2MB)       │  │    │
│  │  │ • Constant Pool   │  │    │
│  │  │ • Annotations     │  │    │
│  │  │ • Methods         │  │    │
│  │  └───────────────────┘  │    │
│  │  ...                    │    │
│  └─────────────────────────┘    │
│                                 │
│  ┌─────────────────────────┐    │
│  │   Class Metaspace       │    │
  (Compressed Class      │    │
│  │   Space, если включено) │    │
│  │  • Klass структуры      │    │
│  │  • vtables              │    │
│  │  • itables              │    │
│  └─────────────────────────┘    │
└─────────────────────────────────┘

Устройство Metaspace

// Упрощенная структура Metaspace в HotSpot
class Metaspace {
    // Arena-based аллокатор
    Metachunk* _chunks;  // Список чанков

    // Статистика
    size_t _used_words;
    size_t _capacity_words;
    size_t _committed_words;
};

// Чанк метаданных
class Metachunk {
    // Заголовок
    size_t _word_size;
    Metablock* _blocks;

    // Тип: Non-Class (методы, константы) или Class (Klass)
    MetaspaceType _type;
};

ClassLoader Leak - главная причина OOM: Metaspace

public class ClassLoaderLeak {
    // Web-приложение, перезагружаемое в Tomcat
    public void leak() throws Exception {
        while (true) {
            // 1. Создаем изолированный ClassLoader
            URLClassLoader loader = new URLClassLoader(
                new URL[]{new URL("file:///app.jar")},
                null  // Родитель = null (изоляция)
            );

            // 2. Загружаем класс
            Class<?> clazz = loader.loadClass("com.example.SomeClass");
            Object instance = clazz.newInstance();

            // 3. Сохраняем ссылку где-то глобально
            GlobalCache.store(instance);  // УТЕЧКА!

            // 4. ClassLoader не может быть выгружен,
            //    т.к. его классы достижимы через instance
            //    → Metaspace растет с каждой перезагрузкой
        }
    }
}

Диагностика ClassLoader leak:

# 1. Проверка количества ClassLoader'ов
jcmd <pid> VM.classloader_stats

# 2. Dump классов
jmap -clstats <pid>

# 3. Включение логирования загрузки классов
-XX:+TraceClassLoading -XX:+TraceClassUnloading

# 4. Ограничение Metaspace
-XX:MaxMetaspaceSize=256m
-XX:MetaspaceSize=64m

ЧАСТЬ 5: STRING POOL И INTERNING

String Pool: хэш-таблица в Heap

// Внутренняя реализация String Pool (StringTable)
class StringTable {
    // Хэш-таблица с отдельными цепочками
    private static Entry[] table;

    static class Entry {
        final String str;
        final int hash;
        Entry next;
    }

    // Основной метод intern()
    static String intern(String str) {
        int hash = hashString(str);
        int index = hash & (table.length - 1);

        for (Entry e = table[index]; e != null; e = e.next) {
            if (e.hash == hash && str.equals(e.str)) {
                return e.str;  // Существующая строка
            }
        }

        // Добавление новой строки
        Entry newEntry = new Entry(str, hash, table[index]);
        table[index] = newEntry;
        return str;
    }
}

Эволюция String Pool

Java 6 и ранее: В PermGen, фиксированный размер, не очищается

-XX:StringTableSize=1009  # Маленький и фиксированный

Java 7+: В Heap, динамический размер, очищается GC

-XX:StringTableSize=60013  # Размер можно настраивать

Когда использовать intern()?

Антипаттерн:

// НИКОГДА ТАК НЕ ДЕЛАЙТЕ
public void processLine(String line) {
    String interned = line.intern();  // Все строки в pool!
    // Pool заполнится, GC не поможет
}

Возможно правильное использование:

public class TokenProcessor {
    // Ограниченный набор известных токенов
    private static final Set<String> KNOWN_TOKENS =
        Set.of("GET", "POST", "PUT", "DELETE", "HEAD").stream()
            .map(String::intern)
            .collect(Collectors.toSet());

    // Часто используемые enum-like значения
    public void process(HttpMethod method) {
        String m = method.name().intern();  // Только 6 возможных значений
        // Быстрое сравнение через ==
        if (m == "GET") {  // SAFE: "GET" гарантированно interned
            // ...
        }
    }
}

Оптимизация парсера CSV:

public class CSVParser {
    private final Map<String, String> pool = new HashMap<>();

    public String internIfFrequent(String value) {
        // Стратегия: intern только часто повторяющиеся значения
        if (value.length() > 10) return value;  // Длинные строки не intern

        String cached = pool.get(value);
        if (cached != null) return cached;

        // Добавляем только если встречается часто
        if (shouldIntern(value)) {
            String interned = value.intern();
            pool.put(value, interned);
            return interned;
        }
        return value;
    }
}

ЧАСТЬ 6: JIT-КОМПИЛЯЦИЯ - C1, C2, АДАПТИВНЫЕ ОПТИМИЗАЦИИ

Трехуровневая компиляция (Tiered Compilation)

┌─────────────────────────────────────────────────┐
ИНТЕРПРЕТАТОР (Уровень 0)│  • Нулевые накладные расходы на старт          │
│  • Медленное выполнение                        │
│  • Сбор профиля: счётчики, типы, ветвления     │
└─────────────────┬───────────────────────────────┘
                   (1000+ вызовов метода)
┌─────────────────────────────────────────────────┐
C1 (CLIENT) КОМПИЛЯТОР                │
│  • Быстрая компиляция (оптимизации уровня 1)│  • Inlining маленьких методов                  │
│  • Локальные оптимизации                       │
│  • Продолжение сбора профиля                   │
└─────────────────┬───────────────────────────────┘
                   (10000+ вызовов метода)
┌─────────────────────────────────────────────────┐
C2 (SERVER) КОМПИЛЯТОР               │
│  • Агрессивные оптимизации (уровень 4)│  • Глобальный анализ потока данных              │
│  • Escape Analysis и Scalar Replacement│  • Девиртуализация и инлайнинг                 │
│  • Векторизация (Auto-Vectorization)└─────────────────────────────────────────────────┘

Конфигурация компиляции

# Уровни компиляции (0-4)
-XX:CompileThreshold=10000        # Порог для C2
-XX:Tier3InvocationThreshold=2000 # Для C1->C2
-XX:Tier4InvocationThreshold=15000

# Размеры кэшей
-XX:ReservedCodeCacheSize=240m    # Кэш нативного кода
-XX:InitialCodeCacheSize=160m

# Контроль компилятора
-XX:+TieredCompilation           # Включить многоуровневую (дефолт)
-XX:-TieredCompilation          # Только C2 (старт медленнее)
-XX:CompileCommand=exclude,com/example/expensiveMethod

Профилирование и девиртуализация

public class DevirtualizationExample {
    interface Shape {
        double area();
    }

    class Circle implements Shape {
        private final double radius;
        public double area() { return Math.PI * radius * radius; }
    }

    class Square implements Shape {
        private final double side;
        public double area() { return side * side; }
    }

    public double totalArea(List<Shape> shapes) {
        double total = 0;
        for (Shape shape : shapes) {
            total += shape.area();  // Виртуальный вызов
        }
        return total;
    }
}

Процесс оптимизации:

  1. Интерпретатор: Собирает профиль типов
    • Shape#area(): 95% Circle, 5% Square
  2. C1 компилятор: Добавляет проверку типа
    if (shape.getClass() == Circle.class) {
        total += ((Circle)shape).area();  // Прямой вызов
    } else {
        total += shape.area();  // Виртуальный вызов
    }
    
  3. C2 компилятор: Если профиль стабилен
    • Создаёт две специализированные версии цикла
    • Для Circle: полностью убирает проверки
    • Для Square: отдельный редкий путь

Escape Analysis и Scalar Replacement

public class Point {
    private final int x, y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }
}

public int compute() {
    Point p = new Point(10, 20);  // NoEscape: не покидает метод
    return p.getX() + p.getY();
}

// После Scalar Replacement:
public int compute_optimized() {
    // Объект Point не создаётся!
    int p_x = 10;  // Поле разложено в локальную переменную
    int p_y = 20;  // Второе поле разложено
    return p_x + p_y;
}

Условия применения:

  1. NoEscape: Объект не передаётся наружу метода
  2. ArgEscape: Передаётся, но не публикуется
  3. GlobalEscape: Публикуется (не оптимизируется)

Включение/выключение:

-XX:+DoEscapeAnalysis      # Включить (дефолт)
-XX:+EliminateAllocations  # Scalar Replacement (дефолт)
-XX:+PrintEscapeAnalysis   # Логирование

ЧАСТЬ 7: VOLATILE И МОДЕЛЬ ПАМЯТИ JAVA

Модель памяти Java (JMM)

Правила happens-before:

  1. Программный порядок: Действия в потоке происходят в порядке программы
  2. Мониторный замок: Освобождение монитора happens-before последующий захват
  3. Volatile: Запись в volatile happens-before чтение того же поля
  4. Старт потока: Thread.start() happens-before любые действия в потоке
  5. Присоединение потока: Все действия в потоке happens-before Thread.join()
  6. Транзитивность: Если A happens-before B и B happens-before C, то A happens-before C

Реализация volatile на уровне процессора

public class VolatileExample {
    private volatile boolean flag = false;
    private int data = 0;

    public void writer() {
        data = 42;           // (1) Обычная запись
        flag = true;         // (2) Volatile запись
    }

    public void reader() {
        if (flag) {          // (3) Volatile чтение
            System.out.println(data); // (4) Увидит 42
        }
    }
}

Барьеры памяти для x86:

; writer()
mov    [data], 42        ; Store data
; StoreStore барьер (x86 не требует)
mov    [flag], 1         ; Store flag (volatile)
sfence                   ; StoreLoad барьер (x86 требует)

; reader()
lfence                   ; LoadLoad барьер (x86 требует)
mov    rax, [flag]       ; Load flag (volatile)
test   rax, rax
jz     .done
; LoadStore барьер (x86 не требует)
mov    rbx, [data]       ; Load data

false sharing и @Contended

Проблема false sharing:

public class FalseSharing {
    // Два поля в одной строке кэша (64 байта)
    volatile long value1;  // [0-7]
    // ... 56 байт ...
    volatile long value2;  // [56-63]

    // Поток 1: постоянно пишет в value1
    // Поток 2: постоянно читает value2
    // ИНОГДА: строка кэша постоянно инвалидируется
    // → производительность падает в разы
}

Решение с @Contended:

public class PaddedData {
    // JVM добавит 128 байт padding с каждой стороны
    @Contended
    volatile long value1;

    @Contended
    volatile long value2;

    // Расположение в памяти:
    // [value1][128 байт padding][... другие поля ...][128 байт padding][value2]
}

Ручное решение (до Java 8):

public class ManualPadding {
    volatile long value1;
    // Явный padding
    long p1, p2, p3, p4, p5, p6, p7; // 56 байт

    volatile long value2;
    long p8, p9, p10, p11, p12, p13, p14; // Ещё 56 байт
}

Диагностика false sharing:

# Linux: perf для мониторинга кэш-промахов
perf stat -e cache-misses,cache-references java -jar app.jar

# JVM флаги для @Contended
-XX:-RestrictContended        # Разрешить использование вне java.base
-XX:ContendedPaddingWidth=128 # Размер padding (по умолчанию 128)

ЧАСТЬ 8: ПРОФИЛИРОВАНИЕ И ОПТИМИЗАЦИЯ НА ПРАКТИКЕ

Сценарий: высоконагруженный сервис

Исходное состояние:

  • 100k RPS, 95-й перцентиль 200ms, куча 8GB
  • Частые Full GC паузы 2-3 секунды

Шаг 1: Сбор данных:

# 1. JFR для анализа пауз
jcmd <pid> JFR.start duration=60s filename=gc.jfr

# 2. Подробные GC логи
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# 3. Heap dump в момент перед Full GC
-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=/path/to/dumps

Шаг 2: Анализ:

// Типичные проблемы:
// 1. Слишком большие Young/Old соотношения
// 2. Частые промоуты из-за больших Survivor
// 3. Memory leak в кэшах
// 4. Слишком агрессивный allocation rate

Шаг 3: Оптимизация:

# Переход на G1 GC
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=35  # Раньше стартовать concurrent cycle

# Настройка Young Gen
-XX:NewRatio=1                          # Больше Young для short-lived
-XX:SurvivorRatio=6                     # Больше Eden
-XX:MaxTenuringThreshold=5              # Быстрее промоут для medium-lived

# Мониторинг
-XX:+PrintAdaptiveSizePolicy            # Как JVM настраивает размеры
-XX:+PrintTenuringDistribution          # Распределение возрастов

Антипаттерны и их исправление

Антипаттерн 1: Ручные System.gc()

// ПЛОХО
public void processBatch() {
    // ...
    System.gc();  // Full GC пауза в непредсказуемый момент
    // ...
}

// Решение: полагаться на JVM или использовать
// -XX:+ExplicitGCInvokesConcurrent для G1
// -XX:+DisableExplicitGC в продакшене

Антипаттерн 2: Большие массивы в Young Gen

// ПЛОХО: 2MB массив в Eden
byte[] buffer = new byte[2 * 1024 * 1024];

// Решение: прямой аллокатор или настройка
-XX:PretenureSizeThreshold=3M  # Объекты >3MB сразу в OldGen

Антипаттерн 3: String concat в цикле

// ПЛОХО: O(n²) по памяти
String result = "";
for (String item : items) {
    result += item;  // Новый StringBuilder каждый раз
}

// Решение:
StringBuilder sb = new StringBuilder(estimatedSize);
for (String item : items) {
    sb.append(item);
}
String result = sb.toString();

ЧАСТЬ 9: КОНКРЕТНЫЕ КОНФИГУРАЦИИ ДЛЯ РАЗНЫХ СЦЕНАРИЕВ

Микросервис (REST API, 4GB куча)

# G1 с агрессивными целями по latency
-XX:+UseG1GC
-XX:MaxGCPauseMillis=50
-XX:G1HeapRegionSize=4M
-XX:InitiatingHeapOccupancyPercent=30
-XX:ConcGCThreads=2
-XX:ParallelGCThreads=4

# Metaspace ограничения
-XX:MaxMetaspaceSize=128M
-XX:MetaspaceSize=64M

# JIT настройки
-XX:ReservedCodeCacheSize=128M
-XX:InitialCodeCacheSize=64M

Пакетная обработка данных (32GB куча)

# Throughput ориентация
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=8
-XX:GCTimeRatio=99
-XX:MaxGCPauseMillis=500

# Большие объекты
-XX:PretenureSizeThreshold=10M
-XX:SurvivorRatio=10

# Мониторинг
-XX:+PrintGCDetails
-XX:+PrintGCApplicationStoppedTime

Low-Latency система (финансовые транзакции)

# ZGC для субмиллисекундных пауз
-XX:+UseZGC
-XX:MaxGCPauseMillis=1
-XX:ConcGCThreads=4
-Xmx16g
-Xms16g  # Фиксированная куча

# Отключение bias locking для стабильности
-XX:-UseBiasedLocking

# Агрессивная JIT компиляция
-XX:-TieredCompilation  # Только C2
-XX:CompileThreshold=1000

ЧАСТЬ 10: МОНИТОРИНГ И ДИАГНОСТИКА В РЕАЛЬНОМ ВРЕМЕНИ

Утилиты и их назначение

  1. jcmd - универсальная команда:
# Полный список доступных команд
jcmd <pid> help

# Дамп кучи
jcmd <pid> GC.heap_dump filename=heap.hprof

# Статус класса
jcmd <pid> GC.class_histogram

# JFR управление
jcmd <pid> JFR.start duration=60s filename=recording.jfr
  1. jstat - GC статистика:
# Каждую секунду, 10 раз
jstat -gc <pid> 1s 10

# Ключевые метрики:
# S0C/S1C: Survivor capacity
# S0U/S1U: Survivor used
# EC/EU: Eden capacity/used
# OC/OU: Old capacity/used
# YGC/YGCT: Young GC count/time
# FGC/FGCT: Full GC count/time
  1. async-profiler - низкоуровневый профилировщик:
# Профилирование CPU
./profiler.sh -d 30 -f cpu.svg <pid>

# Профилирование аллокаций
./profiler.sh -d 30 -e alloc -f alloc.svg <pid>

# Профилирование contended locks
./profiler.sh -d 30 -e lock -f lock.svg <pid>

Настройка логов GC для анализа

# Подробные логи с временными метками
-Xlog:gc*,gc+age=trace,gc+heap=debug:file=gc.log:uptime,level,tags

# Для G1 отдельно
-Xlog:gc+g1*=debug,gc+phases=debug:file=g1.log

# Парсинг логов утилитами
# 1. GCViewer: визуализация
# 2. gceasy.io: онлайн анализ
# 3. jClarity Censum: коммерческий инструмент