- Published on
JVM, Memory Management и Performance
- Authors

- Name
- dima853
- @_dima853
- Объясните полный жизненный цикл объекта в куче (Heap). От создания до сборки мусора, включая поколения (Young Gen, Old Gen), Eden, S0, S1.
- Что такое Garbage Collection (GC)? Объясните основные алгоритмы (Mark-Sweep, Mark-Compact, Copying) и их trade-offs.
- Опишите различия между Serial, Parallel, CMS, G1 и ZGC сборщиками мусора. В каких сценариях какой предпочтительнее?
- Что такое Stop-The-World (STW) паузы? Как разные GC влияют на их длительность и частоту?
- Объясните, что такое "memory leak" в Java. Приведите конкретные примеры из практики (например, в статических коллекциях, кэшах, незакрытых ресурсах).
- Что такое Metaspace (Java 8+) и чем она отличается от PermGen? Что вызывает OutOfMemoryError: Metaspace?
- Объясните String Pool (String Table). Как работает метод
intern()и когда его использование оправдано? - Что такое Escape Analysis и как она помогает в оптимизации? (Связь с Stack Allocation и Scalar Replacement).
- Опишите структуру памяти Java-потока (Stack Memory). Что хранится во фрейме метода (local variables, operand stack, reference to runtime constant pool)?
- Что такое JIT-компиляция (C1, C2/C1 и C2 (Tiered Compilation))? Что такое "профилирование" кода и деоптимизация?
- Объясните принцип работы
volatileпеременной. Что такое "happens-before" и как это обеспечивает видимость изменений между потоками? - Что такое false sharing (ложное разделение) и как его избежать? (Например, с помощью
@Contended).
Ответы на вопросы:
*в этой статье есть упрощения
1. Жизненный цикл объекта в куче: от аллокации до реинкарнации
Создание (Allocation):
- Подавляющее большинство объектов аллоцируются в Eden Space (Young Generation). Аллокация происходит через механизм Pointer Bump (
TLAB— Thread-Local Allocation Buffer), что сводит операцию к инкременту указателя — O(1), почти не требующему синхронизации. - Крупные объекты (порог зависит от 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 — автоматизированная система управления динамической памятью, освобождающая объекты, недостижимые для исполняемой программы.
Алгоритмы:
Mark-Sweep (Пометка-Очистка):
- Фаза 1 (Mark): Обход графа достижимости от GC Roots. Помечаются живые объекты.
- Фаза 2 (Sweep): Линейный проход по всей памяти. Непомеченные (мертвые) блоки помечаются как свободные.
- Trade-offs: Производит фрагментацию. Низкие накладные расходы, но приводит к "дырчатой" куче, что деградирует производительность аллокации и может вызвать OOM при нехватке непрерывного пространства.
Copying (Копирование):
- Разделяет память на два полупространства (
FromиTo). - Живые объекты копируются из
FromвTo. После копирования все пространствоFromсчитается свободным. - Trade-offs: Требует в 2 раза больше памяти (половина всегда пуста). Не фрагментирует память. Крайне эффективен, если большинство объектов умирают молодыми. Используется только в Young Generation.
- Разделяет память на два полупространства (
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Гб.
- Делит кучу на ~2000 регионов. Собирает регионы с наибольшим количеством мусора (
- 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, а логическая ошибка в коде.
Канонические примеры:
- Статические коллекции (Классика):
public class LeakyClass { private static final List<byte[]> STATIC_CACHE = new ArrayList<>(); public void processData(byte[] data) { STATIC_CACHE.add(data); // Объект data вечно достижим через статическое поле } } - Неконтролируемые кэши (Guava Cache, Caffeine без политики вытеснения):
Cache<Key, Value> cache = Caffeine.newBuilder().build(); // Нет expireAfterWrite или maximumSize // Кэш растет бесконечно. - Незакрытые ресурсы (
InputStream,Connection,Session): Ресурсы часто держат ссылки на внутренние буферы или объекты в native memory. Решение:try-with-resources. - Слушатели событий (Listeners) и внутренние классы: Неотписка от слушателя, сохраненного в глобальном контексте, держит ссылку на внешний класс.
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возникает при:- Достижении лимита (
-XX:MaxMetaspaceSize). - Утечке метаданных (ClassLoader Leak): Частая причина — контейнеры (Tomcat, OSGi), где перезагружаются приложения, но старый ClassLoader удерживается (например, через поток или статическую ссылку), не позволяя выгрузить его классы.
- Достижении лимита (
7. String Pool (String Table): Механизм дедупликации
- String Pool — хэш-таблица (
Hashtable) в heap (ранее в PermGen), хранящая канонические (interned) экземплярыString. - Правила:
- Строковые литералы (
"text") добавляются в Pool на этапе загрузки класса. 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 применяет оптимизации:
- 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 не создается. - Stack Allocation: Частный случай Scalar Replacement. Теоретическое размещение на стеке, но в HotSpot реализовано именно как разложение.
- Lock Elision (Устранение блокировок): Если монитор объекта
NoEscape(например, synchronized-блок на локальном объекте), блокировка удаляется, так как она не может быть contended в другом потоке.
- Scalar Replacement (Разложение на скаляры): Если объект
Активация: Включена по умолчанию (-XX:+DoEscapeAnalysis). Эффективна для короткоживущих, локальных объектов (DTO, итераторы, билдеры).
9. Память потока (Stack Memory): Архитектура фрейма
Каждый поток JVM имеет приватный стек, создаваемый при его запуске. Стек состоит из фреймов (stack frames), помещаемых (push) при вызове метода и вынимаемых (pop) при его завершении (нормальном или исключении).
Структура фрейма метода:
- Local Variable Array (LVA): Массив переменных метода, индексируемый с 0.
this(для не-статических методов) хранится вLVA[0].- Параметры метода — в
LVA[1],LVA[2], ... - Локальные переменные — в последующих слотах.
- Каждый слот 32 бита (
int,float,reference).long/doubleзанимают 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) - 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++).Семантика:
- Видимость (Visibility): Запись в
volatile-переменную одним потоком гарантированно становится видна всем последующим чтениям этой переменной из других потоков. - Запрет переупорядочения (Ordering): JVM и процессор не могут переставить операции чтения/записи
volatileпеременной с другими операциями памяти таким образом, что это нарушило бы правило happens-before.
- Видимость (Visibility): Запись в
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 locking (с volatile).
12. False Sharing (Ложное разделение): Скрытый враг производительности
- False Sharing — деградация производительности в многопоточных системах, возникающая, когда два независимых частомодифицируемых поля (
M1иM2), принадлежащие разным объектам (или разным элементам массива), попадают в одну и ту же строку кэша (cache line, обычно 64 байта) процессора. - Механизм: Процессоры поддерживают когерентность кэшей по протоколу MESI. Если поток на ядре 1 изменяет
M1, вся строка кэша помечается как "модифицированная" (Modified), что инвалидирует эту же строку кэша на ядре 2, даже если там лежит толькоM2. Ядро 2 при доступе кM2вынуждено перечитывать строку из памяти, хотя само значениеM2не изменилось. Это вызывает каскадную инвалидацию и "гонку" за строкой кэша. - Последствие: Кажущиеся независимыми операции начинают синхронно конкурировать, вызывая резкое падение масштабируемости.
Решение — выравнивание (Padding, @Contended):
Классический паддинг (до Java 8): Добавление "пустых" полей для разнесения критичных полей по разным строкам кэша.
class Counter { volatile long count1; private long p1, p2, p3, p4, p5, p6, p7; // Паддинг ~56 байт volatile long count2; }@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 кучи) │
├──────────────┼─────────────────┼────────────────────────────┤
│ EDEN │ SURVIVOR 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
}
}
}
Механика аллокации:
- Pointer Bump в TLAB:
current_ptr += object_size - Zeroing memory: JVM обнуляет память для безопасности
- Установка Mark Word:
mark = hash/age/lock_bits - Установка 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
Условия промоута:
- Возрастной порог:
age >= MaxTenuringThreshold(обычно 15) - Размер Survivor: Если Survivor переполнен, самые старые объекты промотятся
- Крупные объекты: >
-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 │
│ Address │ 0000 │ Mark │ Unused │
└─────────┴──────┴──────┴──────────────────────┘
Преимущества:
- 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;
}
}
Процесс оптимизации:
- Интерпретатор: Собирает профиль типов
Shape#area(): 95% Circle, 5% Square
- C1 компилятор: Добавляет проверку типа
if (shape.getClass() == Circle.class) { total += ((Circle)shape).area(); // Прямой вызов } else { total += shape.area(); // Виртуальный вызов } - 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;
}
Условия применения:
- NoEscape: Объект не передаётся наружу метода
- ArgEscape: Передаётся, но не публикуется
- GlobalEscape: Публикуется (не оптимизируется)
Включение/выключение:
-XX:+DoEscapeAnalysis # Включить (дефолт)
-XX:+EliminateAllocations # Scalar Replacement (дефолт)
-XX:+PrintEscapeAnalysis # Логирование
ЧАСТЬ 7: VOLATILE И МОДЕЛЬ ПАМЯТИ JAVA
Модель памяти Java (JMM)
Правила happens-before:
- Программный порядок: Действия в потоке происходят в порядке программы
- Мониторный замок: Освобождение монитора happens-before последующий захват
- Volatile: Запись в volatile happens-before чтение того же поля
- Старт потока:
Thread.start()happens-before любые действия в потоке - Присоединение потока: Все действия в потоке happens-before
Thread.join() - Транзитивность: Если 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: МОНИТОРИНГ И ДИАГНОСТИКА В РЕАЛЬНОМ ВРЕМЕНИ
Утилиты и их назначение
- 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
- 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
- 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: коммерческий инструмент