RU | EN | DE

JIT-компилятор (Just-In-Time)

Что это и как работает

  • JVM сначала интерпретирует байткод. Горячие участки (часто вызываемые методы/циклы) профилируются и JIT компилирует их в нативный код.
  • Tiered Compilation (по умолчанию):
    C1 (быстро, меньше оптимизаций) → C2 (медленнее компиляция, агрессивные оптимизации). Возможен OSR (On-Stack Replacement) — “пересадка” горячего цикла на скомпилированный код прямо посреди выполнения.
  • Ключевые оптимизации: inlining (встраивание вызовов), escape analysis (раскладывание по полям/скаляризация, размещение на стеке), узкоспециализированные intrinsics (System.arraycopy, Math.*), loop unrolling, vectorization.

Как наблюдать и управлять

Флаги (для локального анализа):

-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
-XX:CompileThreshold=10000        # порог "горячести"
-XX:TieredStopAtLevel=1..4        # ограничить уровни Tiered
-XX:+DoEscapeAnalysis             # обычно включено

Типичные ошибки

  • Сравнивают производительность “холодного” кода без прогрева.
  • Пишут микробенчмарки без JMH ⇒ меряют JIT/GC, а не код.
  • Пессимизируют JIT: частые виртуальные вызовы, не финальные классы/методы, чрезмерная абстракция мешают inlining.

Best practices по JIT

  • Прогревай код перед измерениями (или используй JMH).
  • Ключевые горячие методы делай маленькими и “inline-friendly”.
  • Избегай лишней boxing/unboxing в горячих путях, особенно в Stream с автопакетированием.
  • Кэшируй вычисления, убирай полиморфизм там, где важен inlining (sealed/final).

Память JVM: Heap, Stack, Metaspace (+ ещё области)

Карта памяти

  • Heap — объекты. Делится на Young (Eden + Survivor S0/S1) и Old.
    Быстрое выделение через TLAB (Thread-Local Allocation Buffer) и “bump-the-pointer”.
  • Stack — кадры вызовов для каждого потока: локальные переменные, ссылки, адрес возврата.
  • Metaspace — метаданные классов (замена PermGen с Java 8). Растёт в native-памяти.
    Контролируется -XX:MaxMetaspaceSize.
  • Code Cache — скомпилированный JIT-код (-XX:ReservedCodeCacheSize).
  • Direct memory (off-heap NIO) — управляется ОС (-XX:MaxDirectMemorySize).

Быстрая настройка размеров

-Xms2g -Xmx2g                    # min/max heap
-XX:MaxMetaspaceSize=512m        # верхняя граница Metaspace
-XX:ReservedCodeCacheSize=256m

Сборщики мусора (очень кратко)

  • G1 (по умолчанию в 17+) — региональный, предсказуемые паузы, хороший дефолт.
  • ZGC / Shenandoah — низкие паузы, большие heaps, продвинутые (если нужна минимальная latency).
  • Общие понятия: Stop-the-World, safepoints, card table, write barriers.

Типичные ошибки

  • Маленький heap ⇒ частые GC-паузы, OutOfMemoryError.
  • Утечки через статические коллекции/кэши или слушатели (refs не снимаются).
  • Не учитывают off-heap (DirectByteBuffer, память нативных драйверов) и винят heap.

Best practices по памяти

  • Начинай с G1, убедись, что размер heap разумный (минимум = максимум для стабильности).
  • Мониторь метаспейс и direct memory.
  • Используй профилирование allocations; уменьшай краткоживущие объекты в горячем коде.
  • Для кэшей — soft/weak ссылки по необходимости, ограничивай размер.

Инструменты диагностики

1. jconsole (простая телеметрия через JMX)

  • Запуск: jconsole → выбрать процесс (локально) или подключиться по JMX.
  • Что смотреть:
    • Memory: графики heap/metaspace, кнопка Perform GC (только для диагностики).
    • Threads: состояния потоков, Detect Deadlock.
    • MBeans: параметры приложений/пулов (если экспонированы).
  • Когда полезен: первое приближение — увидеть рост heap, количество GC, deadlock в один клик.

2. jvisualvm (GUI-профилировщик/анализ дампов)

  • Запуск: jvisualvm (или VisualVM отдельной установкой).
  • Функции:
    • Sampler CPU/Memory (меньше overhead) и Profiler (точнее, но тяжелей).
    • Открыть heap dump (*.hprof), посмотреть Top Consumers (кто ест память), инспектировать граф достижимости.
    • Плагины: VisualGC (мотивирующий граф поколений heap).
  • Когда полезен: найти утечки, горячие методы, подтвердить гипотезы до/после фикса.

3. jmap (heap-снимок и гистограмма)

Основные команды:

jmap -histo <PID>               # топ классов по числу/объёму
jmap -histo:live <PID>          # только живые объекты (после GC)
jmap -dump:format=b,file=heap.hprof <PID>  # дамп heap
  • Использование: посмотреть, чем забит heap сейчас; выгрузить дамп и открыть в VisualVM/YourKit/JProfiler.

4. jstack (thread dump)

jstack -l <PID>                 # снять дамп потоков с мониторами
jstack -m <PID>                 # со стеком нативного кода

Что искать:

  • Потоки в BLOCKED (монитор/lock), WAITING/TIMED_WAITING (ожидание), RUNNABLE (CPU).
  • Циклические захваты (deadlock): в конце дампа обычно есть секция “Found one Java-level deadlock”.
  • “Hot thread”: поток, который постоянно в RUNNABLE с одной и той же трассой.

Полезное дополнение (не просили, но маст-хэв):
jcmd <PID> VM.native_memory summary — сводка по native/метаспейсу;
jcmd <PID> GC.heap_info | GC.class_histogram | Thread.print.

Мини-кейсы (код → как диагностировать)

1. Утечка памяти через статический список

public class Leak {
    private static final List<byte[]> bag = new ArrayList<>();
    public static void main(String[] args) throws Exception {
        while (true) {
            bag.add(new byte[1_000_000]); // 1 MB
            Thread.sleep(50);
        }
    }
}

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

  1. jconsole → Memory растёт ступеньками, GC не помогает → подозрение на удержание.
  2. jmap -histo:live <PID> → много byte[].
  3. jmap -dump:format=b,file=heap.hprof <PID> → открыть в VisualVM, найти GC Roots → увидишь статическое поле Leak.bag. Фикс: ограничить/очищать кэш, использовать WeakReference/SoftReference при необходимости.

2. Deadlock из-за порядка блокировок

class Locks {
    private final Object a = new Object();
    private final Object b = new Object();
 
    void m1() { synchronized (a) { sleep(100); synchronized (b) {} } }
    void m2() { synchronized (b) { sleep(100); synchronized (a) {} } }
}

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

  • jconsole → Threads → Detect Deadlock.
  • jstack -l <PID> → секция “Found one Java-level deadlock” с перечислением нитей и владельцев мониторов. Фикс: строгий порядок захвата (всегда a затем b), или tryLock/таймауты, или более крупноблочная синхронизация.

3. Высокий CPU одной нитью

Симптом: 100% ядра.
Диагностика:

  • top/htop → PID → tid нити с высоким CPU.
  • jstack <PID> несколько раз подряд → одна и та же трасса в RUNNABLE ⇒ горячая точка.
  • Дальше jvisualvm CPU Sampler/Profiler для подтверждения.

Чек-лист поиска проблем (быстрый)

  1. Память: jconsole графики → jmap -histo:live → heap dump → VisualVM.
  2. Паузы/GC: включить GC-логи (-Xlog:gc* в 17+), проверить коллектор/размер heap.
  3. CPU: jstack несколько раз → hot method → jvisualvm Sampler.
  4. Блокировки: jconsole Detect Deadlock → jstack -l.
  5. Native/Metaspace: jcmd VM.native_memory summary, -XX:MaxMetaspaceSize.

Best practices (итого)

  • Для продакшена: G1, адекватный -Xms = -Xmx, лог GC (-Xlog:gc*:file=gc.log).
  • Для анализа: VisualVM/jconsole для первых симптомов, jmap/jstack для фактов.
  • Уменьшай аллоцирование в горячих местах, избегай лишнего boxing и временных объектов.
  • Проектируй блокировки: порядок, таймауты, Lock/StampedLock вчитку.
  • Покрывай микробенчмарки JMH, а не самопальными таймерами.
  • Следи за Metaspace/Code Cache/Direct memory — это тоже падения по OOME.
  • Для кэшей — лимиты и weak/soft ссылки где уместно.