JIT-компиляция (just-in-time, «точно в срок») — это перевод байт-кода или промежуточного представления программы в машинный код во время ее выполнения, а не заранее. Компилируются не все инструкции подряд, а участки, которые реально исполняются, и в первую очередь — часто повторяющиеся («горячие»).
Содержание
Ниже разберем три модели исполнения кода (интерпретация, AOT, JIT) и явно разведем их, как JIT находит горячий код и оптимизирует его, как это устроено в JVM HotSpot, V8, .NET и PyPy, и в чем честный компромисс: разогрев против пиковой скорости. Без тезиса «JIT всегда быстрее» — это неверно.
Три модели исполнения: интерпретация, AOT и JIT
Процессор понимает только машинный код своей архитектуры. Языки высокого уровня до него надо довести, и есть три разных способа это сделать. Их часто путают, поэтому развожу сразу.
- Интерпретация. Отдельная программа-интерпретатор читает инструкции (исходник или байт-код) и выполняет их по одной, не выпуская машинного кода на диск или в память как самостоятельный артефакт. Быстрый старт, простая переносимость, но в установившемся режиме медленно: накладные расходы на разбор повторяются на каждой итерации.
- AOT-компиляция (ahead-of-time, «заранее»). Весь код переводится в машинный до запуска, отдельным шагом сборки. Классика — C и C++ через gcc или clang, результат вроде
.exeв Windows. Сюда же GraalVM native-image и Native AOT в .NET. Старт мгновенный, разогрева нет, но компилятор не знает, как программа поведет себя на реальных данных, и целевая архитектура фиксируется при сборке. - JIT-компиляция. Гибрид: программа стартует под интерпретатором, а горячие участки компилятор переводит в машинный код уже во время работы, опираясь на собранную статистику выполнения.
Важное уточнение, которое снимает частую путаницу: байт-код обычно готовится AOT (исходник компилируется в байт-код заранее, при сборке), а JIT переводит уже этот байт-код в машинный код во время выполнения. То есть в JVM или .NET работают оба этапа, просто на разных стадиях. При этом «JIT всегда компилирует байт-код» — упрощение: например, V8 строит машинный код из внутреннего представления JavaScript, а PyPy трассирует исполнение, а не транслирует байт-код метод за методом.
| Модель | Когда переводится в машинный код | Разогрев | Знает профиль выполнения | Типичные примеры |
|---|---|---|---|---|
| Интерпретация | не переводится, инструкции исполняются по одной | нет | — | CPython (базовый), Ruby MRI |
| AOT | до запуска, на этапе сборки | нет | нет (без отдельного профиля) | C, C++, Rust, Go, native-image |
| JIT | во время выполнения, для горячих участков | есть | да | JVM HotSpot, V8, .NET (RyuJIT), PyPy |
Как JIT находит горячий код
JIT не компилирует все подряд: это дорого и в основном бесполезно, ведь большая часть кода выполняется редко. Поэтому среда исполнения профилирует программу прямо на ходу.
Работает это так: у методов и циклов есть счетчики вызовов и итераций. Пока счетчик мал, код исполняется интерпретатором. Когда он переходит порог, участок считается «горячим» и отправляется компилятору. Часто применяют несколько уровней (ярусов): сначала быстрый компилятор дает средний по качеству код, а самые горячие места позже перекомпилируются агрессивным оптимизатором.
Ключевое преимущество — адаптивная оптимизация: компилятор видит, какие типы реально приходят, какие ветки берутся чаще, какие вызовы можно встроить (инлайнинг). На этом он делает спекулятивные допущения — например, «в этот метод всегда приходит один и тот же класс». Если допущение нарушится, срабатывает деоптимизация: метод откатывается к менее оптимизированному коду или в интерпретатор, и исполнение продолжается корректно. Это отличает JIT от AOT, где такой информации на этапе сборки нет.
Пример: один и тот же цикл под интерпретатором и JIT
Нагляднее всего разница видна на плотном числовом цикле, запущенном одним и тем же файлом на интерпретаторе (CPython) и на реализации с JIT (PyPy).
# hot_loop.py - один и тот же код для CPython и PyPy
def compute(n):
total = 0
for i in range(n):
total += i * i
return total
if __name__ == "__main__":
print(compute(1000)) # 332833500 - сумма квадратов чисел 0..999
Результат детерминирован: compute(1000) дает 332833500 в обеих средах. Различается не ответ, а скорость. Запустите файл с большим n и замером времени:
python3 hot_loop.py # базовый CPython - интерпретация байт-кода
pypy3 hot_loop.py # PyPy - трассирующий JIT компилирует горячий цикл
На плотном арифметическом цикле PyPy обычно в разы быстрее CPython: его трассирующий JIT замечает горячий цикл и переводит его в машинный код, тогда как CPython разбирает байт-код на каждой итерации. Конкретные числа сильно зависят от версии, машины и характера цикла, поэтому привожу это как порядок эффекта, а не как фиксированную цифру. Обратная сторона видна на коротких скриптах: там разогрев JIT не окупается, и выигрыша может не быть вовсе.
Понаблюдать за компиляцией можно и в Java: запуск с флагом java -XX:+PrintCompilation печатает лог по мере того, как HotSpot компилирует разогревшиеся методы. Точные строки лога зависят от версии JDK и режима, поэтому их стоит смотреть на своей среде, а не заучивать.
JIT в реальных платформах
Каждая платформа реализует JIT по-своему, но идея горячего кода и ярусов повторяется.
| Платформа | Что исполняет | Интерпретатор / базовый ярус | Оптимизирующий JIT |
|---|---|---|---|
| JVM HotSpot | байт-код Java (и других JVM-языков) | интерпретатор + C1 | C2 |
| V8 (Chrome, Node.js) | JavaScript / WebAssembly | Ignition (интерпретатор) | TurboFan |
| .NET (CoreCLR) | CIL/MSIL байт-код | — | RyuJIT |
| PyPy | Python | интерпретатор | трассирующий JIT |
Несколько деталей по каждой.
- JVM HotSpot использует многоуровневую компиляцию (tiered compilation). Уровень 0 — интерпретатор, уровни 1-3 — клиентский компилятор C1 с разной степенью профилирования, уровень 4 — серверный C2. C1 дает быстрый старт, C2 — максимальную пиковую производительность для долгоживущих участков.
- V8 сочетает интерпретатор Ignition и оптимизирующий компилятор TurboFan; между ними добавлены промежуточные ярусы (Sparkplug, Maglev) для более плавного разгона. Названия и наличие ярусов зависят от версии движка.
- .NET (CoreCLR) переводит байт-код CIL в машинный код через RyuJIT при первом обращении к методу. Параллельно у платформы есть AOT-режимы (ReadyToRun, Native AOT) для быстрого старта без разогрева.
- PyPy — альтернативная реализация Python с трассирующим JIT (построена на RPython): он отслеживает горячие циклы и компилирует именно исполненные трассы. Базовый CPython исторически был чистым интерпретатором; в CPython 3.13 (2024) появился экспериментальный JIT, выключенный по умолчанию.
Плюсы и минусы: разогрев против пиковой скорости
JIT — это компромисс, а не бесплатное ускорение.
Сильные стороны. JIT видит поведение программы на реальных данных и оптимизирует под него: специализирует по фактическим типам, встраивает горячие вызовы, отбрасывает недостижимые ветки. Он подстраивается под конкретный процессор при запуске, поэтому один и тот же байт-код едет на разных машинах и везде компилируется под местную архитектуру.
Слабые стороны. Есть разогрев: пока горячий код не скомпилирован, программа работает медленнее (сначала интерпретация). Растет потребление памяти — в процессе живут и скомпилированный код, и данные профилирования, и сам компилятор. Задержки менее предсказуемы: компиляция и деоптимизация могут давать паузы, что критично для систем реального времени.
Отсюда честное сравнение с AOT, без абсолютов. Для долгоживущих серверных нагрузок JIT за счет профиля нередко выходит на уровень оптимизированного нативного кода или обгоняет наивную сборку. Для короткоживущих процессов (утилиты командной строки, serverless-функции, небольшие скрипты) AOT или нативная компиляция обычно выигрывают: там нет времени на разогрев. А AOT с профилем (PGO) способен приблизиться к JIT, используя статистику прошлых запусков. Вывод «JIT всегда быстрее» неверен — выбор зависит от времени жизни процесса и профиля нагрузки.
Выводы
- JIT-компиляция переводит байт-код или промежуточное представление в машинный код во время выполнения, компилируя прежде всего горячие участки.
- Три модели исполнения разные: интерпретация не выпускает машинный код, AOT готовит его до запуска, JIT — на ходу и с учетом профиля.
- Главный козырь JIT — адаптивная оптимизация по данным времени выполнения; расплата за нее — разогрев, лишняя память и менее предсказуемые задержки.
- В боевых платформах используется многоярусность: быстрый старт (интерпретатор или C1/Ignition) плюс агрессивный оптимизатор (C2/TurboFan/RyuJIT) для горячего кода.
- JIT не всегда быстрее AOT: на долгих серверных нагрузках он силен, на коротких процессах чаще выигрывает нативная сборка.
Где применяется / связь с практикой
Понимание JIT нужно, когда вы разбираетесь, почему программа «разгоняется» не сразу, настраиваете производительность JVM или .NET, выбираете между JIT и AOT для serverless или CLI, читаете логи компиляции при профилировании. Это уровень, где язык встречается с процессором и памятью, — область системного программирования.
Освойте тему на практике
Разобраться с тем, как устроены компиляторы, среды исполнения и работа с памятью на низком уровне, помогает курс Системное программирование. Посмотреть формат и глубину занятий, не оплачивая курс, можно на открытых уроках Otus — там разбирают конкретные темы вживую с преподавателями.
Смежные темы: Введение в кодирование информации.
FAQ
JIT-компилятор и интерпретатор — это одно и то же? Нет. Интерпретатор исполняет инструкции по одной и не выпускает машинного кода, а JIT именно порождает машинный код для горячих участков. На практике они работают вместе: сначала интерпретация, затем компиляция разогревшихся мест.
Что такое деоптимизация? Это откат скомпилированного метода назад — в менее оптимизированный код или в интерпретатор, — когда спекулятивное допущение JIT нарушилось (например, в метод пришел неожиданный тип). Программа при этом продолжает работать корректно.
Можно ли запустить программу без JIT? Да. Есть AOT-режимы: GraalVM native-image для Java, Native AOT в .NET, а базовый CPython исполняет код интерпретатором. Это полезно там, где важен мгновенный старт и предсказуемость, а не пиковая пропускная способность.



