Язык и JVM
Что это такое
Исходный код на Java компилируется не в машинные инструкции конкретного процессора, а в байт-код — компактный набор команд виртуальной машины. Дальше в дело вступает среда выполнения: она загружает классы, проверяет их, исполняет байт-код, распоряжается памятью и переводит часто выполняемые участки в машинный код. У этой двухступенчатой схемы два следствия: один и тот же собранный артефакт идёт на любой платформе с совместимой средой выполнения, а производительность программы меняется по мере прогрева.
Почему это важно
Понимание этого разделения объясняет большую часть особенностей Java и её поведения в эксплуатации. Типы проверяются при компиляции исходного кода, поэтому большинство ошибок обнаруживается до первого запуска, а память освобождается автоматически: это убирает целый класс ошибок и добавляет паузы, которые нужно измерять, а не игнорировать. Запуск уступает нативной программе, однако установившаяся пропускная способность часто выше. Поэтому долгоживущий сервер подходит хорошо, а утилита командной строки, которую запускают тысячу раз, — нет.
Ключевые идеи
Компиляция проверяет типы, среда выполнения исполняет байт-код
Компилятор проверяет, что каждый вызов метода и каждое присваивание согласуются с объявленными типами, и до тех пор, пока это не так, байт-код не появляется. Именно это делает рефакторинг большой кодовой базы выполнимым: переименованный или удалённый метод становится ошибкой компиляции у каждого, кто его вызывает, а не отказом во время работы у одного.
исходник .java -> компилятор -> байт-код .class -> виртуальная машина -> машинный код
Объекты живут в куче и освобождаются автоматически
Каждый объект выделяется в управляемой куче; ссылки на него отслеживаются, а память, до которой больше нельзя добраться, освобождается. Программа никогда не освобождает память явно, поэтому висячие ссылки невозможны, а временем жизни занимается сборщик. Компромисс в том, что сбор занимает процессорное время и может приостанавливать выполнение. Поэтому выбор сборщика и размер кучи — эксплуатационные решения, а не значения по умолчанию.
JIT-компилятор оптимизирует то, что действительно выполняется
Код начинает исполняться интерпретатором, а горячие пути компилируются в машинный код с оптимизациями, которые опираются на наблюдение, например на встраивание метода, чья реализация почти всегда одна и та же. Замеры поэтому должны идти достаточно долго, чтобы достичь устойчивого режима. Микробенчмарк на одну секунду измеряет интерпретатор, а не программу.
Загрузка классов ленивая и может отказать во время работы
Классы загружаются при первой необходимости, поэтому отсутствующая зависимость или несовместимая версия отказывает в этот момент, а не при запуске. Отсюда и характерная картина: конфликты зависимостей в Java всплывают ошибкой глубоко внутри запроса, а не отказом стартовать. Поэтому дерево зависимостей нужно смотреть, а не угадывать.
Сборка мусора — это выбор настройки с заметными последствиями
Разные сборщики оптимизируют либо пропускную способность, либо короткие паузы, и выбор одного из них — утверждение о нагрузке: пакетная задача терпит длинные паузы, торговая система — нет. Размер кучи взаимодействует со всем этим. Поэтому полезный подход — измерять паузы и пропускную способность под реальной нагрузкой, а не принимать значение по умолчанию.
Запуск и память — цена платформы
Среда выполнения, которой нужно загрузить и проверить классы и прогреть свой компилятор, стартует медленнее нативной программы, и её базовая память больше. Для сервиса, который работает неделями, это несущественно, а для функции, которую вызывают на каждый запрос, может решать всё. Разницу сделали заметной контейнеры, и потому нативная компиляция для языков JVM стала привлекательной.
Практика
Посмотрите, как программа прогревается
Напишите небольшой бенчмарк, который выполняет одно и то же вычисление много раз, и записывайте время каждой партии.
Готово, когда: вы можете указать партии, где время перестало улучшаться, и объяснить, что среда выполнения делала иначе.
Найдите скрытый отказ зависимости во время работы
Возьмите проект и уберите библиотеку из его classpath во время выполнения, оставив её в classpath компиляции, а затем вызовите код, который ею пользуется.
Готово, когда: вы можете процитировать ошибку, называющую класс, которого не хватает, и сказать, почему она появилась в этот момент, а не при запуске.
Частые ошибки
- Замер до прогрева среды выполнения — измерение отражает работу интерпретатора, а не программы; исправить: замеряйте достаточно долго, чтобы режим установился, и отбрасывайте прогрев.
- Игнорирование пауз сборки мусора — всплески задержки появляются без изменений в коде; исправить: измеряйте паузы под реальной нагрузкой и выбирайте сборщик осознанно.
- Уверенность, что зависимость компиляции нужна и во время работы — отказ приходит внутри запроса; исправить: просмотрите и зафиксируйте всё дерево зависимостей.
- Произвольный размер кучи — сборщик работает с неверным бюджетом; исправить: подбирайте размер кучи по измеренному объёму живых данных и следите за паузами.
- Сервис на JVM для короткой команды — запуск съедает всё время работы; исправить: оставьте среду выполнения для долгоживущих процессов или компилируйте нативно.
Что читать дальше
- Java (05-backend-data/04-java) — язык и платформа, которые описывает этот дочерний урок, поэтому начните с него.
- Объекты и проектирование типов (05-backend-data/04-java/02-objects-and-type-design) — то, к чему применяется проверка типов компилятором.
- Сборка и зависимости (05-backend-data/04-java/03-builds-and-dependencies) — как собирается classpath и артефакты на нём.