Gymterview
middle

Что такое JIT-компиляция и какие уровни компиляции существуют

JIT (Just-In-Time) компиляция — это механизм JVM, при котором байт-код компилируется в нативный машинный код во время выполнения программы, а не заранее. Это позволяет выполнять оптимизации, невозможные при статической компиляции, на основе реального профиля выполнения.

Аналогия из жизни: представьте переводчика, который сначала переводит документ устно (интерпретация — быстро начать, но медленно при повторных обращениях). Заметив, что один и тот же абзац запрашивают часто, он записывает перевод (JIT-компиляция). В следующий раз вместо повторного перевода он просто зачитывает готовый текст.

Интерпретатор vs JIT

Пример
Байт-код (.class)
      |
      v
 Интерпретатор ---- медленное выполнение, быстрый старт
      |
      | (метод вызван N раз - "горячий" код)
      v
  JIT-компилятор -- быстрое выполнение, затраты на компиляцию
      |
      v
 Нативный код ----- максимальная производительность

Уровни компиляции (Tiered Compilation)

С Java 8 по умолчанию включена многоуровневая компиляция (-XX:+TieredCompilation), объединяющая два компилятора:

Уровень Компилятор Описание
0 Интерпретатор Интерпретация байт-кода, сбор базового профиля
1 C1 (Client) Простая компиляция без профилирования
2 C1 (Client) Компиляция с ограниченным профилированием
3 C1 (Client) Компиляция с полным профилированием
4 C2 (Server) Агрессивная оптимизация на основе собранного профиля

Типичный путь: 0 -> 3 -> 4 (интерпретация -> C1 с профилированием -> C2 с оптимизацией).

Ключевые оптимизации JIT

Inlining (встраивание методов) — вызов метода заменяется его телом, устраняя overhead вызова:

Пример
// До инлайнинга
public int square(int x) { return x * x; }
public int compute(int n) { return square(n) + square(n + 1); }

// После инлайнинга (JIT делает автоматически)
public int compute(int n) { return n * n + (n + 1) * (n + 1); }

Escape Analysis (анализ побега) — если объект не покидает метод, JIT может:

  • Scalar replacement — разложить объект на отдельные переменные
  • Stack allocation — разместить объект на стеке вместо heap
  • Lock elision — устранить синхронизацию на объекте
Пример
public int sumPoints() {
    Point p = new Point(3, 4);  // объект не "убегает" из метода
    return p.x + p.y;
    // JIT заменит на: return 3 + 4; (scalar replacement)
}

On-Stack Replacement (OSR) — JIT заменяет интерпретируемый код на скомпилированный прямо во время выполнения метода, не дожидаясь следующего вызова. Особенно полезно для длинных циклов.

Диагностика JIT-компиляции
# Показать компилируемые методы
-XX:+PrintCompilation

# Логирование в Java 9+ (Unified Logging)
-Xlog:jit+compilation=info

# Показать инлайнинг
-XX:+PrintInlining

# Отключить C2 (только C1)
-XX:TieredStopAtLevel=1

# Отключить JIT полностью (только интерпретация)
-Xint

# Увеличить порог компиляции (по умолчанию ~10000 вызовов)
-XX:CompileThreshold=5000

GraalVM JIT (Graal Compiler)

GraalVM предоставляет альтернативный JIT-компилятор, написанный на Java (вместо C++ компилятора C2):

  • Написан на Java — легче разрабатывать и поддерживать
  • Продвинутые оптимизации (partial escape analysis)
  • Можно использовать как замену C2: -XX:+UseJVMCICompiler
  • Основа для GraalVM Native Image (AOT-компиляция)

Частые ошибки

  • Микробенчмарки без прогрева JIT — результаты не показывают реальную производительность (используйте JMH)
  • -Xint в production — отключает JIT, производительность падает в 10-100 раз
  • Слишком большие методы (> 300 строк) — JIT не будет их инлайнить и может не оптимизировать
  • Предположение, что JIT оптимизирует всё — только горячий код компилируется

На собеседовании: объясните суть JIT (компиляция горячего кода в native во время выполнения) и путь 0 -> 3 -> 4 (интерпретация -> C1 -> C2). Назовите ключевые оптимизации: inlining, escape analysis, OSR. Частая ловушка — вопрос о деоптимизации: JIT может откатить спекулятивную оптимизацию, если она оказалась неверной. Упомяните JMH для корректных бенчмарков.