Компиляторы
JVM: архитектура и байткод
В 1995 году Sun выпустила Java с лозунгом 'Write Once, Run Anywhere'. JVM обеспечила это: .class файл запускается на любой JVM - Windows, Linux, macOS, Solaris. В 2024 году JVM запускает не только Java, но и Kotlin, Scala, Groovy, Clojure, JRuby - все компилируются в один и тот же .class формат.
- **HotSpot JVM** от Oracle используется в большинстве production Java приложений. Adaptive JIT компиляция (C1 + C2): C1 быстро компилирует горячий код, C2 - агрессивно оптимизирует самый горячий. Крупные microservice платформы (Kafka, Elasticsearch, Cassandra) - всё на HotSpot.
- **GraalVM** - альтернативная JVM с Native Image: AOT компиляция Java в нативный бинарник через SubstrateVM. Spring Boot на GraalVM Native: startup за 50ms вместо 3-5 секунд, память в 5x меньше. Используется в serverless (AWS Lambda, Quarkus).
- **Kotlin** компилируется в тот же JVM bytecode что и Java - полная interoperability. Kotlin coroutines компилируются в state machine (suspend fun -> invokedynamic-based continuation). Kotlin/Native и Kotlin/JS компилируют в LLVM IR и JS - один язык, три платформы.
Class File Format
Java .class файл - бинарный формат, описывающий один класс или интерфейс. Структура строго стандартизирована в JVM Specification. Любой JVM (HotSpot, GraalVM, Dalvik) должен корректно читать .class файлы - это основа 'write once, run anywhere'.
Constant pool - ключевая структура .class файла: все строки, имена классов, сигнатуры методов хранятся в нём в одном экземпляре, а код ссылается на индексы. Это сжатие: имя `java/lang/String` встречается сотни раз в коде, но хранится один раз. 0xCAFEBABE выбрана командой Gosling как шутка - они часто обедали в кафе "Grateful Dead's Mojo Cafe" (полное название пришлось сократить).
Что означает magic number `0xCAFEBABE` в начале .class файла?
JVM Bytecode
JVM bytecode - stack-based набор из 256 opcodes (0x00-0xFF). Каждый метод имеет Code attribute с байткодом, max_stack (максимальная глубина стека), max_locals (число локальных переменных). Типизация встроена в opcodes: `i` = int, `l` = long, `f` = float, `d` = double, `a` = reference.
invokedynamic (Java 7, 2011) - революция в JVM bytecode. Позволяет языкам (Groovy, Scala, Kotlin, динамический Java) реализовывать вызовы с произвольной dispatch семантикой через MethodHandle. Без invokedynamic: lambda в Java компилировалась в анонимный класс (1000 байт для `() -> x + 1`). С invokedynamic: lambda - просто invokedynamic с bootstrap method, без отдельного класса.
Зачем JVM имеет `iload` (int) и `aload` (reference) вместо одного `load`?
Class Loading
Class loading - динамическая загрузка .class файлов при первом использовании класса. JVM не загружает все классы при старте - только те, что реально нужны. Это lazy loading: import java.util.* не загружает LinkedList если он не используется.
Hotspot JVM имеет Class Data Sharing (CDS): часто используемые классы (java.*, javax.*) сериализуются в shared archive (classes.jsa) при установке JDK. При запуске программы archive memory-mapped - классы не читаются с диска и не верифицируются заново. Это ускоряет startup на 20-40%. AppCDS расширяет это на пользовательские классы - Spring Boot приложение запускается в 2x быстрее с AppCDS.
Почему JVM использует lazy class loading (при первом использовании) вместо eager (при старте)?
Bytecode Verification
Bytecode verifier - компонент JVM, проверяющий корректность байткода перед выполнением: типобезопасность, отсутствие переполнений стека, корректность jumps. Без верификации JVM была бы небезопасна для запуска untrusted кода (апплеты, OSGi, Tomcat).
Stack Map Frames (Java SE 6, JSR 202) - аннотации в байткоде, явно указывающие типы стека на каждом jump target. Верификатор Java SE 8+ проверяет только согласованность с этими frame, не вычисляет их сам. Это снизило время верификации с O(N^2) до O(N). Android (Dalvik/ART) использует похожий подход в dex формате. GraalVM добавляет специализированный verifier, который работает в 3x быстрее HotSpot на холодном старте.
Зачем JVM верифицирует байткод если код написан на Java (надёжный компилятор)?
Итоги
- **.class формат** содержит constant pool, методы с байткодом, атрибуты. Magic `0xCAFEBABE`. Строго стандартизирован - любая JVM читает одинаково.
- **JVM bytecode** - stack-based, типизированный (iadd vs fadd vs ladd). invokedynamic (Java 7) открыл JVM для динамических языков и lambda.
- **Class loading** - lazy: только при первом использовании. Иерархия ClassLoader'ов. CDS ускоряет startup через shared archive.
- **Verification** - dataflow анализ типов стека до выполнения. Гарантирует безопасность независимо от источника .class файла. Stack Map Frames: O(N).
Связанные темы
JVM - полная реализация всех концепций управляемой среды исполнения:
- Байткод и виртуальные машины — JVM - конкретная реализация stack-based VM с верификатором и JIT
- V8: JavaScript — V8 решает схожие задачи (JIT, деоптимизация) для динамического языка - интересное сравнение подходов
- Линковка и загрузка — Class loading - аналог динамической линковки в управляемой среде с верификацией
Вопросы для размышления
- GraalVM Native Image компилирует Java AOT в нативный бинарник. Это противоречит 'Write Once, Run Anywhere' - бинарник для Linux x86-64 не запустится на macOS ARM64. Какие trade-offs делает команда при выборе между AOT (Native Image) и традиционным JIT?
- invokedynamic позволяет реализовать произвольную dispatch семантику. Как Kotlin использует invokedynamic для coroutines - что именно заменяется и какой overhead остаётся?
- JVM верификатор проверяет типы стека статически. После JIT компиляции верификация уже не нужна. Как JVM обрабатывает deoptimization (возврат к интерпретируемому коду после JIT) - нужна ли повторная верификация?