На чем была написана Java?
Собственно стало интересно и гугл не смог дать мне ответа. Везде пишут что разрабатывался с оглядкой на C++, но вот на чем собственно написан(или с помощью чего) нигде не оговаривается. Так же интересно на чем пишут JVM для новых ОС или устройств. Кто знает расскажите, интересно ведь.
Отслеживать
Viacheslav
задан 17 ноя 2011 в 15:23
Viacheslav Viacheslav
715 3 3 золотых знака 11 11 серебряных знаков 23 23 бронзовых знака
Странно, что никто не упомянул то, что помимо кода на c и c++ у jvm есть ещё ассемблерные вставки
6 июл 2015 в 18:31
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Действительно интересный вопрос, поискал нашел ответ, который меня в принципе удовлетворил: What is java written in
Отслеживать
ответ дан 17 ноя 2011 в 15:33
Sergii Kozlov Sergii Kozlov
2,468 16 16 серебряных знаков 13 13 бронзовых знаков
Да почти все языки так или иначе написано на С++ и(или) на С ^^
17 ноя 2011 в 15:56
Только не «языки», а их интерпретаторы, компиляторы и т.д.
17 ноя 2011 в 17:52
Естественно, интерпретаторы, виртуальные машины, компиляторы, в том числе JIT, пишутся на С или С++ (во всяком случае, их ядра) как на языках, дающих наибольшую скорость выполнения программ.
Отслеживать
ответ дан 17 ноя 2011 в 17:17
23.9k 2 2 золотых знака 37 37 серебряных знаков 69 69 бронзовых знаков
Разве Java интерпретатор? Там JIT компиляция вроде.
17 ноя 2011 в 17:23
Это сильно зависит от используемого JDK . В ответе на stackoverflow есть примеры JDK , написанных на Java .
17 ноя 2011 в 17:54
Это скорее экзотика. Теоретически можно написать на любом языке, средства которого это позволяют. Наиболее распространенный вариант — С/С++ по указанным мною причинам.
Что такое Java?
Java – широко используемый язык программирования для написания интернет-приложений. Язык Java широко использовался на протяжении более двух десятилетий. Миллионы приложений Java используются и сегодня. Java – это многоплатформенный, объектно-ориентированный и сетецентрический язык, который сам по себе может использоваться как платформа. Это быстрый, безопасный и надежный язык программирования для всего: от мобильных приложений и корпоративного ПО до приложений для работы с большими данными и серверных технологий.
Для чего используется язык программирования Java?
Для чего используется язык программирования Java?
Поскольку Java является бесплатным и универсальным языком, на нем создаются локализованные и распространяемые программы. Типичные примеры использования Java см. ниже.
1. Разработка игр
Многие популярные мобильные, компьютерные и видеоигры созданы на Java. Даже современные игры, в которых используются передовые технологии, такие как машинное обучение или виртуальная реальность, создаются с использованием технологии Java.
2. Облачные вычисления
Язык Java часто называют WORA (Write Once and Run Anywhere – «Напиши один раз, запускай где угодно»), что делает его идеальным для децентрализованных облачных приложений. Поставщики облачных услуг выбирают язык Java для запуска программ на широком спектре базовых платформ.
3. Большие данные
Язык Java используется для механизмов обработки данных, которые могут работать со сложными наборами данных и большими объемами данных в режиме реального времени.
4. Искусственный интеллект
Java – это кладезь библиотек машинного обучения. Благодаря своей стабильности и скорости язык стал выбором № 1 для разработки приложений искусственного интеллекта, таких как обработка естественного языка и глубокое обучение.
5. Интернет вещей
Язык Java используется для программирования датчиков и аппаратного обеспечения периферийных устройств, которые могут независимо подключаться к Интернету.
Почему язык Java так популярен среди современных разработчиков ПО?
Секрет популярности Java заключается в простоте его использования. Некоторые причины, по которым разработчики предпочитают Java другим языкам программирования см. ниже.
Высококачественные учебные ресурсы
Поскольку Java существует уже давно, для новых программистов доступно множество учебных ресурсов. Подробная документация, исчерпывающие печатные материалы и курсы помогают разработчикам на протяжении всего обучения. Кроме того, новички могут начать писать код на Core Java, прежде чем переходить на Advanced Java.
Встроенные функции и библиотеки
При использовании Java разработчикам не нужно каждый раз писать новую функцию с нуля. В качестве альтернативы Java предоставляет богатую экосистему встроенных функций и библиотек для разработки ряда приложений.
Активная поддержка сообщества
У Java много активных пользователей и сообщество, которое может поддержать разработчиков, когда они сталкиваются с трудностями при написании кода. ПО платформы Java также регулярно поддерживается и обновляется.
Высококачественные инструменты разработки
Язык Java предлагает различные инструменты для поддержки автоматизированного редактирования, отладки, тестирования, развертывания и управления изменениями. Эти инструменты делают программирование на Java экономичным и быстрым.
Независимость от платформы
Код Java может работать на любой базовой платформе, такой как Windows, Linux, iOS или Android, без перезаписи. Таким образом, язык особенно эффективен в современной среде, где приложения запускаются на нескольких устройствах.
Безопасность
Пользователи могут загружать ненадежный код Java по сети и запускать его в безопасной среде, в которой он не может причинить никакого вреда. Ненадежный код не может заразить хост-систему вирусом, а также не может читать или записывать файлы с жесткого диска. Уровни безопасности и ограничения в Java также легко настраиваются.
Как работает Java?
Все языки программирования являются средством общения с машинами. Аппаратное обеспечение машины реагирует только на электронную связь. Языки программирования высокого уровня, такие как Java, играют роль моста между человеческим и аппаратным языком. Для использования Java разработчики должны понимать две вещи.
1. Язык Java и API
Это внешний интерфейс между разработчиком и платформой Java.
2. Виртуальная машина Java
Это внутренняя связь между платформой Java и базовым аппаратным оборудованием. Подробное описание см. ниже.
Что такое виртуальная машина Java?
Виртуальная машина Java действует как дополнительный уровень абстракции между платформой Java и базовым аппаратным обеспечением машины. Исходный код Java может работать только на тех машинах, на которых установлена виртуальная машина Java (Java Virtual Machine, JVM). Ответ на вопрос, зачем нужна виртуальная машина Java, лежит в истории программирования.
История программирования
Когда естественные языки программирования были впервые разработаны, они подразделялись на две большие категории в зависимости от того, как они взаимодействовали с базовым оборудованием.
- Компиляторы. Вся программа написана в естественном английском синтаксисе с помощью компилирующих программ, а затем язык компилирует (или переводит) весь код в машинный код. После этого скомпилированный код запускается на оборудовании.
- Интерпретаторы. С помощью интерпретирующих программ каждое высокоуровневое выражение кода интерпретируется в машинный код в режиме реального времени. Зафиксированные предписания немедленно выполняются аппаратным обеспечением перед просмотром следующего предписания.
Среда времени выполнения Java
Программа Java была первым языком, объединившим оба описанных выше метода с использованием JVM. Компилятор кода Java называется виртуальной машиной Java. Любой файл Java сначала компилируется в байт-код. Байт-код Java может работать только в JVM. Затем JVM интерпретирует байт-код для запуска на базовой аппаратной платформе. Поэтому, если приложение работает на компьютере с Windows, JVM интерпретирует его для Windows. Однако при работе на платформе с открытым исходным кодом, такой как Linux, JVM интерпретирует его для Linux.
Как программировать на Java
Чтобы начать программировать на Java, необходимо установить Java Edition в системе. Существует четыре основные версии Java:
- стандартная версия Java (Java Standard Edition, Java SE);
- корпоративная версия Java (Java Enterprise Edition, Java EE);
- микроверсия Java (Java Micro Edition, Java ME).
Что такое Java SE?
Стандартная версия Java – это основная платформа программирования Java. Такая версия содержит все библиотеки и API, которые нужны любому программисту для разработки на Java. Open Java Development Kit (OpenJDK) – это бесплатная реализация Java SE с открытым исходным кодом.
Amazon Corretto – это бесплатная многоплатформенная, готовая к использованию версия пакета средств разработки OpenJDK. Сервис можно использовать в качестве замены для многих дистрибутивов Java SE, при этом он поставляется с бесплатной долгосрочной поддержкой Amazon. Сервис позволяет запускать одну и ту же среду в облаке, локально и на локальном компьютере.
Что такое AWS SDK для Java?
AWS SDK для Java упрощает использование сервисов AWS, предоставляя упорядоченный набор библиотек, с которыми знакомы разработчики Java. AWS SDK for Java также поддерживает более высокий уровень абстракций для упрощения разработки. Доступны библиотеки Java с открытым исходным кодом, ориентированные на AWS, а также примеры кода и справочное руководство по API Java.
Примеры кода SDK содержат примеры кода на языке Java и реальные примеры работы с сервисами AWS, которые ускорят разработку собственных приложений. Кроме того, справочник по Java API описывает работу API в новейшей версии AWS SDK для Java. Кроме того, приводятся образцы запросов, ответов и список ошибок для поддерживаемых сетевых протоколов.
Начните работать с AWS SDK для Java, используя руководство для разработчиков, или посетите центр разработчиков AWS Java.
Как устроена Java Virtual Machine и её экосистема: подробный гайд
Иван Углянский рассказал, как устроена JVM, что с ней не так и зачем Python, Ruby и другие языки переезжают на виртуальную машину Java.


Иллюстрация: Mark Kingston / WikiMedia Commons / Colowgee для Skillbox Media

Антон Сёмин
Пишет об истории IT, разработке и советской кибернетике. Знает Python, JavaScript и немного C++, но предпочитает писать на русском.

об эксперте
JVM engineer. Работает в Excelsior@Huawei над виртуальными машинами Java, компиляторами и новыми языками программирования. Один из основателей и лидеров JUGNsk, член программного комитета SnowOne Conference.
ссылки
Я занимаюсь JVM более 10 лет. Начинал в новосибирской компании Excelsior, которая разрабатывала свою реализацию виртуальной машины Java — Excelsior JET. Потом мы вошли в состав Huawei, но команда осталась прежней.
Что такое JVM и как она спасла программистов от профессионального «подгорания»
До появления Java, примерно 25 лет назад, многие приложения писали на С или С++. У этих языков есть проблема: разработчику приходится думать, на какой операционке и архитектуре процессора будет работать его код. Например, если он пишет программу под Linux, то, скорее всего, она не запустится на Windows или MacOS. Поэтому код приходилось пичкать директивами условной компиляции или писать отдельную версию для каждой операционки.
Самую сильную боль вызывали приложения с GUI — код графических компонентов для разных операционок был совершенно разным. Причина в том, что C и C++ довольно близки к железу, а значит, сишник должен учитывать архитектуру процессора и тип операционной системы. Понятно, что никакой кроссплатформенностью здесь и не пахнет.
Ещё в 1960-е годы у инженеров появилась идея: писать программы не для конкретного железа, а для абстрактного «исполнителя». Программы на Java как раз пишутся для такого исполнителя — виртуальной машины, или Java Virtual Machine (JVM). Java-разработчик не задумывается, на какой платформе будет запускаться его код. В то же время виртуальная машина не знает, что исполняет инструкции на Java, ведь она принимает и исполняет байт-код.
В этом и было «уникальное торговое предложение» Java: разработчики писали программу под Windows, а она запускалась на macOS почти без изменений. Сейчас это звучит вполне естественно, а 25 лет назад казалось настоящим чудом.

Кроссплатформенность в Java обеспечивается явным разделением уровней языка и реализации.
Языковой уровень. Разработчики пишут код на языке Java, синтаксис и семантика которого описаны в Java Language Specification. После этого специальным инструментом, который называется javac, исходный код компилируется в байт-код Java. При этом происходит проверка синтаксиса, и в случае его нарушения разработчик получает сообщение об ошибке от javac.
Что здесь важно:
- На этом этапе нет ничего платформенно-специфичного, весь код на языке Java (как и байт-код Java) универсален.
- Байт-код Java — это язык, предназначенный не для людей, а для машин. Обычному разработчику его читать не нужно.
Уровень реализации. Полученный байт-код Java передаётся на вход виртуальной машины Java. И вот как именно она будет его исполнять, описано уже в другой спецификации — Java Virtual Machine Specification. Со всеми особенностями конкретной операционной системы или архитектуры процессора тоже разбирается JVM, без влияния на исходный код на языке Java. Таким образом, происходит перенос ответственности: разработчики на Java о таких неприятных вещах больше не думают, им достаточно просто взять правильную JVM (например, для Linux x64). А все OS/arch-специфические нюансы решают разработчики JVM.
Язык Java и сама JVM разрабатываются параллельно, при этом новые фичи языка зачастую требуют поддержки внутри JVM. С другой стороны, различные реализации JVM развиваются и сами по себе, например в них появляются новые, более эффективные алгоритмы сборки мусора или оптимизации кода.
У разделения языка и его реализации есть замечательное следствие: разработчику не обязательно ограничиваться языком Java, ведь виртуальная машина понятия не имеет, откуда взяли байт-код, который пришёл к ней на вход, был ли это изначально Java-код или что-то другое. То есть программу можно писать на любом языке, который транслируется в байт-код. И таких языков — целое семейство: Kotlin, Clojure, Groovy и так далее. Например, программы на Kotlin компилируются с помощью kotlinc в class-файлы, а затем подаются на вход JVM.
Как соотносятся JDK и Open JDK
Java Development Kit (JDK) — это комплект инструментов для разработки приложений на Java. В него входят несколько компонентов, которые позволяют разрабатывать и запускать приложения:
- Javас. Это компилятор, который преобразует исходники Java в class-файлы или формат jar.
- JVM. Виртуальная машина, которая исполняет байт-код.
- Стандартная библиотека. Набор модулей, классов, объектов и функций, которые доступны в языке.
- Документация. В ней содержится справочная информация об инструментах данной версии JDK.
Когда появляется новая версия языка, все эти компоненты обновляются.
В случае стандартной JVM (например, HotSpot) запуск Java-приложения выглядит так: сначала с помощью javac вы компилируете исходный код в class-файлы (или jar-файлы), и именно их вы можете поставлять в качестве приложения.
Чтобы приложение запустилось на хосте, там должна быть установлена виртуальная машина. Тогда пользователь пишет в командной строке: java -jar имя вашего jar-файла> — и программа запускается.
С нашей виртуальной машиной сценарий запуска приложения был другой: мы заранее компилировали class-файлы в один исполняемый exe-файл. Поэтому он работал на компьютере пользователя без каких-либо предустановленных JVM. Но, по сути, JVM никуда не исчезла — просто все её части были слинкованы в один экзешник.
В спецификации Java Virtual Machine почти ничего не сказано о том, как именно её реализовывать — только что должно получиться на выходе. Например, вы не можете явно освободить память из под объекта, когда он вам уже не нужен. Этим занимается специальный компонент JVM — Garbage Collector. Но вот как именно он работает, в спецификации не сказано.
Так и было задумано: спецификация позволяет создавать разные JVM, реализации которых в чём-то отличаются, но при этом удовлетворяют требованиям спецификации. И это замечательно, пусть расцветают сто цветов! Такие различия в реализации дают разным виртуальным машинам особые конкурентные преимущества: у кого-то более крутой сборщик мусора, у кого-то быстрый стартап, а кто-то реализовал более мощные оптимизации в компиляторе.
Первую JVM, как и язык Java, разработали в компании Sun Microsystems. Когда Sun опубликовала спецификацию, Oracle стала работать над своей JRockit, а IBM — над OpenJ9. Наша Excelsior JET вышла в свет в 2000 году.
Изначально все JVM были закрытыми, но в 2006 году Sun опубликовала исходники своей JVM HotSpot и компилятора javac, а потом и стандартной библиотеки Java. Это дало старт проекту OpenJDK — полностью открытой реализации JDK. Через какое-то время Sun была поглощена корпорацией Oracle, но при этом проект OpenJDK никуда не делся и до сих пор остаётся главной open-source-площадкой для разработки Java и JVM.
OpenJDK — это эталонная реализация, на которую ориентируются разработчики других виртуальных машин. Проект распространяется под GPL 2 + Classpath Exception, благодаря чему любая компания или разработчик, которым хватит квалификации, смогут форкнуть OpenJDK и начать разрабатывать свою собственную JVM. Конечно же, по условию лицензии их реализация тоже будет open source.
Существуют и реализации JVM, которые никак не связаны с OpenJDK. Ведь не только Sun Microsystems и Oracle создавали JVM. Другой пример — это IBM J9, которая, кстати, тоже открыла исходный код и стала называться OpenJ9.
Некоторые компании продают свои виртуальные машины. Например, у Azul есть свободная сборка Zulu и закрытая коммерческая Azul Zing. В последней есть уникальный сборщик мусора C4, который гарантировал низкие паузы ещё до того, как это стало мейнстримом. А также технология ReadyNow!, которая позволяет «запомнить» список всех оптимизаций кода и «проиграть» его на старте JVM, выводя машину на пиковую производительность.
Excelsior JET тоже была закрытой и платной. Она обеспечивала быстрый стартап, низкое потребление памяти и хорошую пиковую производительность во многих сценариях. Всё это мы получали за счёт статической компиляции, в отличие от динамической компиляции на лету в HotSpot.
Однако сегодня люди чаще пользуются бесплатными сборками, основанными на OpenJDK. Существуют как стандартные сборки OpenJDK от Oracle, так и сборки от других компаний и разработчиков, имеющих отношение к разработке OpenJDK (зачастую их публикуют контрибьюторы в OpenJDK не из Oracle).
В таких сборках могут быть дополнительные фичи, не включённые по умолчанию в стандартные сборки от Oracle. Либо такие альтернативные сборки прошли дополнительную сертификацию, например для использования с «КриптоПро». При этом такие сторонние билды проходят все стандартные тесты, поэтому гарантируется, что ваши программы будут работать как надо и пользоваться ими можно абсолютно безопасно и бесплатно.
Некоторые компании не только выкладывают свои билды, но и оказывают платную поддержку пользователям. Допустим, возникла проблема с виртуальной машиной: ваше приложение работает неожиданно медленно или даже крашится! Всякое бывает, ведь JVM — это тоже программа, в которой могут быть свои баги. Тогда инженеры этой компании оперативно разберутся в проблеме и решат её.
В целом вариантов виртуальных машин и их сборок очень много — в рамках этой статьи все не перечислить. Есть сборки от Oracle, Red Hat, Azul, IBM, BellSoft, Amazon и Microsoft. Сообщество Java-чемпионов написало замечательный документ, в котором перечислило все варианты и в целом разъяснило ситуацию с многообразием JVM и их сборок. Документ называется Java Is Still Free, рекомендую его всем к прочтению.
Как и зачем другие языки переходят на JVM
Java — это не просто язык, а целая философия. И она оказалась настолько востребованной, что у других языков программирования стали появляться реализации под JVM. Например, Jython и JRuby.
Главная причина появления таких реализаций — перформанс. За 25 лет человечество научилось делать хорошо оптимизированные JVM. Так почему бы за счёт этого не повысить производительность программ на Python и Ruby?
С другой стороны, так можно получить хорошую связь с экосистемой Java. Если мы запускаем код на JVM, он лучше взаимодействует с другими модулями на JVM. Значит, можно пользоваться и Java-библиотеками.
У Kotlin немного другая история. Когда язык только появился, было решено компилировать его в байт-код Java и запускать на JVM. Таким образом, реализация языка заключалась в написании хорошего транслятора — kotlinc — из исходного кода .kt в class-файлы. А реализация низкоуровневых компонентов типа сборщиков мусора или взаимодействия с операционной системой делегировалась уже существующим JVM.
Относительно недавно создатели Kotlin сделали Kotlin Native — технологию компиляции Kotlin в нативный код напрямую, без JVM. В нём низкоуровневые компоненты, такие как менеджер памяти, уже реализованы самостоятельно.
Хотя на словах кажется, что любой язык программирования легко перевести на JVM (достаточно написать транслятор в class-файлы), на деле всё оказывается намного сложнее. Грамотно отобразить фичи языка в байт-код Java и при этом получить хорошую производительность — нетривиальная задача. Посмотрите хотя бы на JRuby.
Чарльз Наттер часто делится опытом поддержки различных фич Ruby в проекте JRuby и рассказывает о сложностях, с которыми сталкивается. Чем дальше исходный язык от Java, тем сложнее задача трансляции. Но, думаю, всё возможно, и JRuby — наглядный тому пример.
Какие конкуренты есть у JVM
Самый известный конкурент Java Virtual Machine — платформа .NET и их виртуальная машина для реализации C#.
В начале 2000-х Microsoft делала свою виртуальную машину Java — Microsoft J++. Но из-за того, что корпорация не соблюдала спецификацию, Sun подала на неё судебный иск. Microsoft проиграла все суды и лишилась права делать виртуальную машину для Java. Есть мнение, что это стало одной из причин появления C#.
Java и C# решают одни и те же задачи и обладают одними и теми же преимуществами: строгая типизация, сборка мусора, безопасность — всё это совпадает. Это managed-языки широкого применения.
Раньше между платформами была одна принципиальная разница: Java был кроссплатформенным, а .NET и C# работали только на Windows. Но с тех пор, как появился .NET Core, C# тоже стал мультиплатформенным.
Отмечу, что сами языки Java и C# довольно разные, и C# развивается быстрее: там регулярно появляются интересные фичи, которые в Java приходят с большим опозданием или не приходят вообще. Поэтому как язык C# в целом выглядит интереснее.
Зато у Java мощная реализация и хорошо оптимизированные виртуальные машины. Это стало возможным благодаря «гонке вооружений» между разработчиками виртуальных машин. Вендоры на протяжении 20 лет соревновались, кто сделает самую крутую оптимизацию, лучший алгоритм сборки мусора, более быстрый стартап и так далее. Поэтому по многим показателям реализация Java выглядит куда лучше C#. Некоторые вещи, которые уже давно есть в JVM, только-только появляются в .NET.
Есть и другие языки, которые претендуют на роль конкурента Java, но они отличаются гораздо сильнее. Например, Python, который недавно занял 1-е место в рейтинге TIOBE. Он популярен в Data Science, AI, машинном обучении, скриптах и так далее. К сожалению, динамическая типизация не позволяет держать на нём слишком большую кодовую базу.
Ещё есть безумно популярный JavaScript, но его интересы с Java не очень пересекаются, так как сегодня Java во фронтенде не используется. Хотя когда-то были и такие планы (привет устаревшей технологии Java-апплетов).
Где Java хорош, а где — нет
Java широко используют в бэкенде, то есть во всех приложениях, которые работают на стороне сервера. Его главный конкурент — C#, но Java всё-таки значительно лидирует, потому что считается более кроссплатформенным. Хотя последнее уже не совсем правда. Также стоит отметить, что у Java банально есть фора по времени по сравнению с C#. Он появился на 10 лет раньше, поэтому успел захватить умы разработчиков и кодовые базы.
Почему Java, а не «швейцарский нож» С++? Просто Java удобнее и безопаснее для жизни, или, как принято говорить, не даст выстрелить себе в ногу. Разработчику не нужно думать об управлении памятью и сегфолтах, которые могу прилететь хоть откуда. Поэтому, когда дело касается будничных задач бэкенда, скорость разработки на Java и C# гораздо выше, чем на C++.
Java плохо подходит для приложений, которым требуется высокая производительность и гарантированное время отклика. Тем не менее некоторые пишут на Java и такие программы, если готовы мириться с небольшими задержками в микросекунды. Для них выигрыш от скорости разработки и надёжности важнее. Но писать на Java программное обеспечение для кардиостимуляторов, где все элементы должны работать точно, как швейцарские часы, — не стоит.

С другой стороны, приложения для трейдинга всё чаще пишут именно на Java. Казалось бы, где, если не в трейдинге, важен быстрый отклик — чтобы не потерять миллионы из-за скачка цен на акции Tesla? Однако на практике надёжность здесь важнее скорости: лучше торговать медленнее, но спокойно и надёжно, чем написать на С++ быструю биржу, которая через пять минут торгов крашнется. Java гарантирует безопасность, и это круто.
Что не так с JVM
В истории развития любого языка программирования всегда находятся тонкие места или сомнительные решения. Java здесь не исключение, встречаются такие спорные вопросы и в спецификациях Java и JVM.
Система модулей
Одна из спорных тем — Project Jigsaw и система модулей, появившаяся в девятой версии Java. Создатели Java долго делали систему, которая позволяет разбивать Java-программу на отдельные логические составляющие — модули. Это дополнительный уровень абстракции над давно существующими packages в Java.
На неё потратили много времени — ведь нужно было организовать поддержку в самом языке, виртуальной машине и других частях JDK. Система долго не попадала в релизную версию, а когда всё-таки попала, оказалась, мягко говоря, не слишком востребованной пользователями.
Как в языке появляются новые фичи и спецификации? Есть специальный совет Java Community Process (JCP), который рассматривает предложения Java Specification Request (JSR) и голосует за или против. В этом совете есть представители крупных компаний, есть представители user group и даже отдельные люди — в общем, представлены интересы разных сторон Java Community.
Идея новой модульной системы в Java была несколько раз отвергнута JCP (что вообще случается довольно редко), но всё-таки вошла в язык после значительных доработок и послаблений. Тем не менее спустя несколько лет мы видим, что идея так и не пришлась по вкусу Java-разработчикам и модулями пользуются редко.
Дженерики
Дженерики в Java, в отличие от многих других языков, — стираемые. Это означает, что все типовые параметры стираются через javac до Object. В рантайме вы не сможете понять, какой тип вам на самом деле пришёл. У этого подхода есть свои плюсы и минусы, а споры о том, что лучше — стираемые или нестираемые дженерики — продолжаются и по сей день. Тем не менее, выбор в пользу стираемых дженериков в Java сделан не случайно. Подробное обоснование, почему дженерики в Java такие, можно почитать вот в этой статье Брайана Гетца «Background: how we got the generics we have».
Но как бы вы не относились к реализации дженериков в Java, сейчас есть существенная проблема: нельзя использовать примитивные типы в качестве типовых параметров (ведь они несовместимы с Object). Это сокращает выразительную силу языка и вынуждает либо использовать Boxed-типы (что может ухудшить производительность), либо и вовсе специализировать функции под примитивные типы вручную. Исправить это собираются с помощью специализированных дженериков в рамках проекта Valhalla.
Обратная совместимость
Обратная совместимость — один из главных принципов Java, которым совет JCP не пожертвует даже ради самой передовой фичи. От некоторых вещей иногда отказываются, но при этом старый код всё равно должен продолжать работать.
Например, реализация исключений через инструкции JSR/RET встречается в старом байт-коде, а в новом — нет. Но до сих пор во всех тулах нужно уметь поддерживать старый байт-код ради библиотеки, написанной 20 лет назад, но которая так и продолжает использоваться в современных проектах в виде того же старого формата .jar.
С одной стороны, этот принцип позволяет построить стабильную экосистему. Бизнес может не бояться, что выйдет новая версия, которая всё сломает и навсегда заблокирует путь к обновлению. С другой стороны, это тормозит инновации. Возможно, Java развивается медленнее, чем C#, потому, что у нас обратная совместимость — это священная корова, которую нельзя трогать.
Насколько глубоко разработчику нужно знать JVM
На самом деле это очень холиварный вопрос. В основном ответ на него зависит от задач, которые хочет решать джавист.
Думаю, 30% программистов могут спокойно работать и не понимать, что происходит внутри виртуальной машины. Другим 50% достаточно на среднем уровне знать, как устроена JVM, — тогда они будут отличными мидлами и сеньорами. Их работа не в том, чтобы копаться в кишках виртуальной машины, но некоторые знания о её устройстве помогут им писать более эффективный код и избежать многих ошибок. В оставшихся 20% задач без знания деталей всё-таки не обойтись. Получается, чем лучше вы разбираетесь в виртуальной машине, тем более широкий класс задач способны решать.

Если в проекте важна производительность и повысить её с помощью выбора более оптимального алгоритма нельзя, надо разбираться, как работает сборщик мусора и как повысить его эффективность, какие настройки можно поменять, что такое деоптимизация, почему она происходит и так далее.
Java-разработчик, который регулярно погружается в эту тему, со временем сможет стать Performance Engineer. Эти специалисты находят проблемы производительности, анализируют глубинные процессы в JVM и исправляют их. Они решают проблемы не дописыванием кода в виртуальную машину, а настраивают её, как часовщики.
С точки зрения культуры всем разработчикам на managed-языках полезно знать, что происходит под капотом машины, когда они нажимают кнопку Run в IDE. Конечно, не нужно запоминать, что и в какой момент делает каждая деталь JVM. Но пройти вводный курс и понять на концептуальном уровне, что такое интерпретатор, Just In Time Compiler, что значит «умер объект» и так далее, — полезно.
Разработчик, который не знает ответов на все эти вопросы, способен решить довольно много задач. Но однажды он столкнётся с ошибками, причины которых даже не сможет понять. Например, если не задумываться, откуда берётся память и как она возвращается операционной системе, то можно получить от IDE сообщение Out of memory. А всё потому, что программа потребляет слишком много памяти. Разработчик, который знает, как устроена память, постарается оптимизировать код и не создавать горы объектов.
Куда развивается JVM
Сейчас довольно активно развиваются три проекта, которые рано или поздно войдут в новые версии Java.
Loom
Loom — это проект по добавлению виртуальных тредов в Java. С ним треды не мапятся на треды операционной системы, а живут сами по себе. И лишь когда их нужно исполнить, они будут садиться на треды операционки.
Раньше каждому экземпляру класса java.lang.Thread (через которые реализуется многопоточное исполнение) соответствовал один поток операционной системы. Вполне логичное решение, но есть проблема: потоков операционной системы не слишком много, поэтому вряд ли у вас получится создать больше нескольких тысяч потоков, реализованных таким образом. Project Loom предлагает альтернативу — виртуальные потоки, которые уже не связаны соотношением 1 к 1 с потоками виртуальной системы. Вместо этого они время от времени садятся на эти OS threads, чтобы выполнить очередной квант работы.
Идея не новая. Например, в Go так было всегда. Да и вообще о виртуальных тредах говорили уже много десятилетий. И даже в самом Java в 1990-е были грин-треды — схожая идея, где N зелёных потоков назначались на один поток операционной системы (тогда в процессорах было по одному ядру и проблема легковесной многопоточности не стояла так остро). Можно сказать, что Loom — это в некотором смысле реинкарнация зелёных тредов, но сделанная на более серьёзном уровне.
Этот проект очень важен, так как в современном мире микросервисов и серверов с огромным количеством одновременных подключений вам часто хотелось бы создавать десятки и сотни тысяч тредов. Со старым подходом к реализации через OS threads это просто невозможно, а вот с Loom всё получится.
Panama
Panama — это проект, посвящённый взаимодействию между Java и нативным кодом, написанным, например, на C или C++. Конечно, Java мог взаимодействовать с нативным кодом с самого рождения. Основное решение в этой области — Java Native Interface, очень мощный, но медленный и неудобный в использовании механизм. Чтобы взаимодействовать с нативами, приходилось писать некрасивый, громоздкий и, что самое главное, небезопасный код. Я уверен, что любому джависту, который хоть что-нибудь делал с JNI, как минимум не понравилось, а как максимум — он до сих видит кошмары про развалы в JNI.
Чтобы избавить Java-разработчиков от ночных кошмаров, авторы Panama уже много лет работают над своим проектом, который, как Панамский канал, соединяет два мира: нативный и Java. Panama предлагает новый интерфейс и его оптимизированную реализацию на стороне JVM, которая позволит приятно и безопасно работать с нативным кодом, в том числе с off-heap .
Valhalla
Проект Valhalla посвящён введению в Java новых типов объектов, так называемых value-типов, или inline-типов, или же primitive-value-типов — это текущее название. Да, проект настолько сложный, что даже основная его концепция часто меняет имя!
Если коротко, то смысл в том, чтобы выделить подкласс объектов, у которых нет identity. Что это такое? В Java вы можете создать два абсолютно одинаковых объекта с помощью оператора new. Потом сравнить их на == и получить false. Это разные объекты! Они сравниваются не по содержимому (которое одинаковое), а по некоторому уникальному свойству, назначаемому каждому объекту при создании. Вот возможность сравнивать не по содержимому, а по чему-то ещё — это и есть identity.
С другой стороны, можно объявить две переменные типа int, записать в каждую из них значение 42, сравнить на == и получить результат true. Всё потому, что у int никакого identity нет, они сравниваются по содержимому.
Цель Valhalla — дать пользователям возможность самим описывать классы, у которых нет identity, и подготовить к этому уже существующую типовую систему. Как следствие, можно будет, например, заводить плоские поля таких типов внутри других классов (то есть фактически инлайнить их поля внутрь других объектов), работать с плоскими массивами таких объектов и даже получить специализированные дженерики.
Это огромное, фундаментальное изменение всей платформы Java, за которым очень интересно следить и которого стоит ожидать, ведь оно принесёт множество возможностей обычным разработчикам.
На каких языках разрабатывают JVM и библиотеки для Java
Сами виртуальные машины Java обычно пишут на С++, стандартная библиотека же в основном написана на самом Java (хотя иногда и приходится уходить в нативный код, то есть снова в плюсы). Вообще, так повелось, что системщину пишут на плюсах, ведь здесь, с одной стороны, важна производительность, а с другой — связь с низким уровнем, с операционной системой и железом. В то же время стало очевидно, что далеко не все компоненты виртуальной машины обязательно нужно писать на С++. Например, зачем делать компилятор на С++, если есть замечательные managed-языки вроде Java или Scala?
Вот, например, в Oracle Labs разрабатывает проект GraalVM. Он состоит из трёх компонентов:
- КомпиляторGraal — мощный оптимизирующий компилятор, в будущем он, возможно, заменит компилятор C2 из HotSpot. Он не уступает по качеству многих оптимизаций, но написан полностью на Java.
- Truffle Framework — специальный фреймворк для написания интерпретаторов других языков, которые потом запускаются на GraalVM. По сути, это быстрый способ реализовать любой язык программирования на базе GraalVM! Опять-таки, интерпретаторы пишутся на managed-языках.
- Graal Native Image — отдельный режим сборки приложений, который на самом деле представляет из себя целую виртуальную машину, но написанную с некоторыми нарушениями спецификации. Зато она позволяет получить быстрый стартап и низкое потребление памяти. Рантайм для такой виртуальной машины тоже частично написан на Java! Понятно, что не полностью и с разными ограничениями языка (на самом деле там используется отдельный диалект — System Java), но тем не менее это возможно.
Вообще, писать виртуальные машины на managed-языках — давняя мечта системщиков. Ведь, например, компилятор С написан на С? Вот и разработчики виртуальных машин для managed-языков хотят писать их на Java или других managed-языках, а не на опасном и болезненном С++.
Как контрибьютить в OpenJDK
Для начала скажу, что я никогда не контрибьютил в OpenJDK, а занимался разработкой другой, проприетарной и закрытой JVM. Поэтому о процедуре коммита в OpenJDK имею только смутное представление, так что написанному ниже доверяйте на свой страх и риск!
Если бы я задался целью контрибьютить в OpenJDK, то думал бы в двух направлениях:
- Найти что-то небольшое, что получится исправить или улучшить. Это может быть несложный (и низкоприоритетный) баг в баг-трекере OpenJDK или неэффективность какого-нибудь алгоритма в платформенных классах. Исправьте эту проблему, соберите OpenJDK и проверьте, что, например, алгоритм и правда стал работать лучше на каких-нибудь бенчмарках.
Потом напишите в мейлинг-лист, расскажите о сути улучшения и попросите сделать ревью. Наконец, пройдите его, а затем и остальную процедуру коммита. Насколько я знаю, она довольно стандартная, ведь OpenJDK теперь находится на GitHub. Поздравляю, теперь вы контрибьютор в OpenJDK!
По рассказам людей, которые прошли по этому пути, вся процедура может занять довольно много времени, особенно в первый раз, но в целом это вполне реально.
- Устроиться на работу в компанию-контрибьютор. В этом случае вы будете разрабатывать код, который попадёт в OpenJDK, в рабочее время. Однако сложность в том, что таких компаний всё-таки очень мало. Зато попасть в одну из них — действительно круто. Яркий пример такой компании — питерская BellSoft, которая в прошлом году заняла четвёртое место в мире (!) среди внешних (не из Oracle) контрибьюторов в OpenJDK.
На кого подписаться и где следить за новостями JVM
Разработчики виртуальных машин регулярно улучшают свои VM и зачастую делятся новостями о разработке и состоянии тех или иных проектов в рамках OpenJDK и не только. Черпать актуальную информацию можно из мейлинг-листов, конференций и Twitter.
Мейлинг-листы
Мейлинг-лист — это такой почтовый ящик, на который можно подписаться. Подписчики мейлинг-листа получают письма из общей рассылки, но ещё могут отвечать, участвовать в дискуссиях или задавать вопросы. Именно так и общаются разработчики многих языков программирования.
Мейлинг-листов про Java и JVM довольно много, есть свои мейлинг-листы для разных компонентов JDK и разных подпроектов в OpenJDK. Например, есть отдельные мейлинг-листы для упомянутых выше проектов Loom или Valhalla. Так что тут главное — выбрать темы, которые вам интересны, а потом найти соответствующие мейлинг-листы на сайте сообщества Java.
Новые предложения по технологии в первую очередь пишут и обсуждают именно в мейлинг-листах, поэтому в них отражается история развития Java и JVM. Такой канал связи действительно работает, хоть и может показаться немного устаревшим. Если хотите узнать сложные детали, например как работает конкретная часть виртуальной машины, советую подписаться на соответствующий мейлинг-лист.
Конференции
Более привычный и простой для восприятия источник информации — конференции. Разработчики JVM регулярно выступают с докладами, в которых по косточкам разбирают детали реализации, при этом в доступной и понятной форме.
Наибольшая концентрация знаний — на ежегодной конференции Java Virtual Machine Language Summit. Видео выступлений можно посмотреть на YouTube-канале Java. Очень много хардкорных и качественных докладов от разработчиков JVM есть на российских конференциях Joker и JPoint от компании JUG Ru Group.
Сами мы (JUGNsk) тоже организуем свою Java-конференцию SnowOne в Новосибирске. В нашем программном комитете много системщиков, поэтому докладам о внутренностях виртуальных машин на конференции уделяется особое внимание.
Ещё один источник свежей информации — Twitter. Часто там появляются последние новости о Java вообще и JVM в частности. Из англоязычных авторов советую подписаться на аккаунты Брайана Гетца и Марка Рейнхольда — архитекторов языка Java и Java-платформы.
В России тоже есть суперзвёзды и разработчики JVM, например Алексей Шипилёв. Он один из главных контрибьюторов Shenandoah GC, работал в Oracle, а сейчас — в Red Hat. При этом он пишет классные посты (чего стоит одна только серия JVM Anatomy Quarks!) и выступает с отличными докладами о том, как устроена JVM изнутри.
Ещё всегда интересно читать Сергея Куксенко — перформанс-инженера в Oracle. Он разгоняет многие фичи, которые появляются в Java, и интересно об этом рассказывает. А новости о HotSpot и Panama можно найти в аккаунте Владимира Иванова.
В целом в Twitter есть ещё огромное количество как русскоязычных, так и англоязычных крутых инженеров, связанных с разработкой JVM. Так что принцип здесь такой же, как с мейлинг-листами: находите человека, участвующего в определённом проекте в OpenJDK (или за его пределами), и смело подписываетесь!
Резюме:
- Чтобы решать рутинные задачи, джависту не нужно разбираться в деталях JVM. Но перейти на следующий уровень не получится, если хотя бы на концептуальном уровне не понимать, как работает интерпретатор, что такое Just In Time Compilation, как устроена память и так далее.
- JDK — это комплект инструментов Java-разработчика. В него входит: javac, сама виртуальная машина, стандартная библиотека классов и документация. OpenJDK — это эталонная версия JDK с полностью открытым исходным кодом. Основные контрибьюторы в этот проект — это Oracle, но кроме неё есть и много других компаний и разработчиков, которые вносят свой вклад.
- Обычно виртуальные машины пишут на языках C/C++. Но уже есть проекты, в которых JVM, компилятор и рантайм почти полностью написаны на Java или его диалекте.
- Контрибьютить в OpenJDK может любой желающий, но серьёзные изменения в JVM обычно вносят опытные разработчики. Тем не менее возможно найти себе небольшие задачи, с которых и начать этот путь.
Читайте также:
- Чем Rust отличается от плюсов: откровение ветерана C++
- Есть ли место творчеству в программировании: подборка мнений опытных разработчиков
- Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS
Один день из жизни JVM-инженера

Можно разрабатывать на Java, а можно разрабатывать Java. Есть люди, чей код исполняет виртуальная машина — а есть люди, чей код и есть виртуальная машина.
Вроде бы те и другие существуют в одной Java-экосистеме, но задачи совершенно разные. Поэтому редкое место, где они пересекаются и могут что-то поведать друг другу — Java-конференции. Мы проводим их регулярно (уже в апреле будет JPoint). И на предыдущей нашей конференции Иван Углянский dbg_nsk поделился с Java-разработчиками тем, как всё выглядит с его стороны.
Чем он вообще занимается? Почему JVM-инженеры всё так медленно делают? На каком языке стоит писать рантайм, а на каком компилятор? Как «папка бога» в Windows привела к неожиданным последствиям? Может ли «обычный джавист» стать JVM-инженером?
Поскольку все эти вопросы из доклада звучат интересно, мы решили для Хабра сделать его текстовую версию (а для тех, кому удобнее видео, прикладываем ссылку на ютуб). Далее повествование идёт от лица Ивана.
Экосистема Java состоит из двух слоев. Верхний слой — прекрасный город с великолепной архитектурой и красивыми высокими зданиями. В этом городе живут эльфы, которые программируют на джаве. У них есть классные фреймворки, Spring, Hibernate, системы сборки и так далее. Все высокоуровнево. А под этим красивым Ривенделом есть пещера с гномами. Они сидят в своей пещере, света солнечного не видят, копошатся в кишках и делают виртуальную машину. У них совсем другие темы для обсуждения: JIT, сборщики мусора, они больше говорят про C++, чем про джаву, и копаются в байтиках.
Я изначально как раз из этого нижнего мира — тот самый гном, который сидит и делает виртуальные машины. Восемь лет я делал это в компании Excelsior, там была собственная виртуальная машина Excelsior JET. Последние три года я занимаюсь этим в Huawei.
Так что сегодня я расскажу вам, кто такие эти гномы, что они делают и зачем они нужны. Расскажу, как конкретно разрабатываются фичи в JVM в бытовом смысле: как выглядит день JVM-инженера, как он работает? Ну и, конечно же, потравлю байки: за 11 лет я видел много чего интересного в странно работающих Java-приложениях. А еще отвечу на вопрос «Почему вы всё так долго делаете».
Как работает Java?
Начнем с самого высокого уровня. У нас есть какая-то программа на джаве:
public static void main(String[] args) < for (int i = 0; i < 10; i++)< Object obj = new Object(); System.out.println(obj.hashCode()); >> test.java
Что с ней нужно сделать в первую очередь?
Сначала нужно запустить инструмент javac : Java-компилятор, который превратит исходный код на Java в код на джава-байткоде, немного другом языке программирования. Это тоже часть JVM, но мы относимся к ней как к «фронтенду»: как к самому краешку, такому «игрушечному» компилятору, который просто транслирует один язык в другой, почти без всяких оптимизаций. Вот такая у нас фронтенд-разработка, правда, совсем без JavaScript.

Когда у вас появился байткод, его нужно скормить черному ящику, который называется Java Virtual Machine, виртуальная машина. Они бывают разные: классические вроде HotSpot и разные другие реализации, об этом ещё поговорим. А на выходе мы получаем счастье: отлично работающую Java-программу, которая точно соответствует семантике того, что вы написали.

Что такое счастье? Это ассемблер. И на выходе мы должны получить работающий машинный код. Каким-то образом появившийся, каким-то образом исполненный.

Есть разные подходы, как можно получить такой код. Можно проинтерпретировать. К сожалению, в некоторых кругах за джавой до сих пор закрепилось поверье, что это именно интерпретируемый язык, со всеми вытекающими из этого последствиями..
Конечно, в современном мире это не так. Кроме интерпретации у нас есть, например, JIT-компиляция, когда вы на лету компилируете что-то в машинный код, а уже потом он будет исполняться. Также сейчас уже довольно популярна AOT-компиляция джавы, когда вы заранее превращаете весь байткод в машинный код, как будто у вас просто приложение на C++. Раньше по-серьезному таким занимались только мы в Excelsior, но сейчас благодаря GraalVM и Native Image это стало популярнее, и никого этим уже не удивишь.
Кроме компиляторов, есть рантайм. Он должен поддерживать исполнение вашего кода, сгенерированного каким-то образом. Это очень важная часть, и сегодня здесь на конференции много говорил Владимир Воскресенский. О ней тоже еще поговорим.
Теперь конкретнее. Чем в этих частях занимаются JVM-инженеры? Если вы JVM-инженер в компиляторе, то глобально вам нужно породить тот самый машинный код под нужную платформу (хотите на Intel — значит, на Intel, если на ARM — то на ARM). При этом код должен быть корректным, должен сохранять семантику джава-программы. Ну и совсем уже желательно, чтобы он был эффективным, чтобы ваша джава-программа работала быстро после этой самой компиляции.
Если говорить предметно, то вам в компиляторе нужно делать какие-то абсолютно классические вещи, о которых вы наверняка слышали в университете: parsing -> lowering -> code generation. Несколько примеров оптимизации: удаление избыточного кода (DCE), inline, loop unrolling, global value numbering, scalar replacement, register allocation, bounds-checking optimizations, …
Это всё хорошо описано в классической литературе. Например, в одной из самых известных книг по компиляторам, книге дракона «Compilers. Principles, Techniques, and Tools» by Alfred V.Aho, Ravi Sethi, Jeffrey D.Ullman.
Единственное, что стоит отметить: современные компиляторщики находятся на передовом крае этой науки. Чтобы сгенерировать еще более оптимальный, еще более быстро работающий код, нужно не просто пользоваться тем, что было, а придумать какие-то новые алгоритмы и новые подходы, нужно двигать науку вперед. И они этим активно занимаются.
Так как я сам рантаймщик, я буду больше говорить про дела в рантайме. Глобально нам просто нужно помочь полученному машинному коду работать эффективно. Если говорить чуть более предметно, то у нас есть очень много очень непохожих друг на друга задач. Из того, что больше всего на слуху — это, конечно, автоматическое управление памятью. Аллоцировать объекты, собирать мусор. Это, безусловно, важная часть рантайма, поэтому про это есть огромное количество прекрасных докладов. Также это поддержка tiered-компиляции: переход от интерпретатора к более оптимизированному коду (и обратно!).
На самом деле это только вершина айсберга. В рантайме гораздо больше задач, и глобально их можно описать так: реализация высокоуровневых фич языков. Речь про исключения, синхронизацию, трединг, рефлексию, интероп и так далее. Для вас эти фичи работают как-то из коробки. Вы просто берете и выбрасываете исключения. Но чтобы это работало из коробки, кто-то должен был это написать. И это работа для рантайм-инженера.
Вот вам пара экзотических примеров.
Пример 1: Неявные исключения
Начнем с рантайма. Допустим, у меня есть код на С:
struct st < int a, b; >; int main() < struct st * ptr = NULL; ptr->a = 42; >
Есть какая-то структура, два поля. Я завожу указатель на эту структуру, потом записываю туда NULL . И дальше «доступаюсь» к какому-то полю этой самой структуры. Что случается, если я запущу такую программу на С? У меня будет сегфолт. Точнее, будет undefined behavior, но именно в этом случае можно быть уверенным, что у вас будет Segmentation fault (core dumped).
Что будет, если я так сделаю в джаве?
class Test < int a; >public static void main(String[] args)
Мы заводим класс с каким-нибудь полем, возьмем ссылку NULL и попытаемся доступиться к этому полю. Вылетит красивый NullPointerException :
Exception in thread “main” java.lang.NullPointerException: Cannot assign field “a” because “t” is null at Main.main(Main.java:9)
В современных версиях Java он вот даже пишет, где и в какое поле вы пытались что-то записать. Эти исключения можно поймать, обработать, перевыбросить — в общем, ваша программа точно не крэшнулась, всё корректно, и вы можете продолжать с этим работать.
Как это реализовать? Как добиться такого поведения со стороны JVM?
Самое простое, что можно сделать — это полагаться на компилятор. Компилятор мог бы найти все такие места доступа к полям или вызова методов от каких-то объектов и просто автоматически сгенерировать проверки.
public static void foo(Test t) < if (t != null) < // [ COMPILER-GENERATED] t.a = 42; >else < // [ COMPILER-GENERATED] throw new NullPointerException(); >> foo(null);
Если не NULL , то делай что написано, а если NULL , то просто выбрасывай NullPointerException . Некоторые компиляторы так и делают. Но кажется, что если каждый доступ к полю, каждый вызов функции обрамлять проверкой — это же очень дорого.
Даже если вы сможете оптимизировать разные доступы к полям, вынести всё под какое-то одно условие, то всё равно это сильно просадит вашу производительность, потому что в вашем коде появятся какие-то переходы.
Как это работает на самом деле. Напишем вот такой метод foo :
public static void foo(Test t) < t.a = 42; >foo(null);
И чтобы понять, как это работает, надо посмотреть на ассемблер. Добавим PrintAssembly , UnlockDiagnosticVMOptions и видим, что весь наш метод превратился в маленький красивый mov , который просто берет значение 0x2a (то есть 42 в шестнадцатеричной системе) и записывает по нашему офсету C. Офсет C — это офсет первого поля в объекте t. Нет никаких проверок, ничего дополнительного, просто один лаконичный mov :

Более того, у вас рядом появятся комментарии, что это какой-то putfield , и даже будет подписано исключение, которое вас куда-то отправляет. Давайте разберемся, что это такое и как это работает.
Компилятор с рантаймом в этом месте договариваются так: не будем ничего генерировать, пусть взрывается. Пусть, если приходит NULL , у нас случится сегфолт, как в С. Но задача рантайма — обработать и отфильтровать все эти сегфолты. Например, ваш сегфолт очень похож на NullPointerException . Если он случился в точке, где запись в поле говорит, что офсет доступов должен был быть именно 12, ровно по этому офсету поля a , тогда это и правда NullPointerException , стоит остановить происходящий «развал» и выбросить NPE. Но если мы никак не смогли его распознать, тогда это core dump и ваша виртуальная машина правда крэшнулась.

Получается, что такие исключения ( NullPointerException ) вообще бесплатные, когда не случаются. Чаще всего никаких NPE у вас нет, и всё работает максимально быстро.
Однако если вдруг они случились, то это, конечно, дорого. Но, с одной стороны, зачем вы написали такой код, в котором постоянно случаются NPE? С другой стороны, если это и правда горячее место, и раз за разом происходит NullPointerException , то виртуальная машина заметит это (особенно в случае JIT-а), деоптимизирует ваш код и все-таки вставит явную проверку (это будет выгоднее, чем все время крэшиться, перехватывать сегфолт и оборачиваеть его в NPE).
На самом деле этот трюк, когда вы позволяете «развалиться», а потом разбираетесь, что же пошло не так, очень популярен в рантайме. Многие вещи делаются с его помощью. Например, stack overflow. Никто не делает явной проверки в прологе, кончился у вас стек или нет. Вместо этого мы пытаемся достучаться до page guard. Если случится развал — заметим это и скажем, что у вас stack overflow. То же самое с safe points для сборщика мусора. Это супер-инструмент, который помогает оптимизировать производительность, излюбленный трюк, как JIT-, так и AOT-компиляторов. Главное, что вы этого не замечаете. Рантайм здесь всячески работает за вас. С точки зрения сгенерированного кода это абсолютно невидимо, и это здорово.
Пример 2: new -то ненастоящий!
Обратный пример. Когда у нас компилятор делает что-то такое, чтобы рантайм поменьше работал. Я имею в виду удаление избыточных операторов new . Вообще выделять объекты, исполнить оператор new — это очень накладная штука. Даже не потому, что вы саллоцируете память, аллокаторы зачастую быстрые. А потому что вы нагрузите сборщик мусора. Потом ему придется прийти и почистить эту память. Вы таким образом сильно утяжеляете его работу, поэтому любая виртуальная машина старается этого избежать. Как это сделать?
В компиляторе есть замечательный Escape Analysis (анализ утеканий), который поможет вам понять, утекает ли куда-то ваш объект, нужен ли он кому-то за пределами локального метода, который сейчас исполняется, или все-таки нет.
Представим, сделали вы вот тут аллокацию, сказали, что Test obj = new Test();
class Test < int a, b; >public void foo()
И потом исключительно локально, в пределах foo() , пользуетесь этим объектом. Зачем здесь что-то аллоцировать в хипе? Он ведь тут же помрет, и его нужно будет собирать сборщиком мусора. Конечно же никакой аллокации здесь не будет. Вместо этого сработает знаменитая оптимизация Scalar Replacement, которая заменит использование этого объекта на использование его полей. Она как будто взорвет его, аннигилирует на атомы, и мы будем работать с его полями как с обычными локалами. То есть не будет никакой аллокации, просто появятся два локала с нужными значениями, и на этом все.
Это очень эффективная тактика. Во-первых, new не срабатывал, сборщик мусора не напрягался. Во-вторых, вы потратили даже меньше памяти, чем на обычный объект, потому что у обычного объекта есть еще заголовки, а здесь вы просто работаете с полями. В-третьих, поля этого объекта скорее всего попадут на регистры — а значит, мы будем работать с ними гораздо быстрее, чем если бы это был просто объект в памяти.
Посмотрим на другой пример:
class Test < int a, b; >public void bar(Test arg) < arg.b = 10; System.out.println(arg.a + arg.b); >public void foo()
У нас есть аллокация объекта Test , мы немного поиспользовали его внутри метода foo() , а потом передаем его в метод bar() , который тоже сам по себе очень хорош, никакого утекания здесь не происходит. Предположим, что метод bar никак не проинлайнился. По какой-то причине bar остался в коде. Тогда объект уже просто так не взорвешь. Вроде бы и утекания никакого нет. Но с другой стороны, если вы объект расщепите на атомы, то передать внутрь метода bar придется не один указатель на начало объекта, а все его поля, придется его копировать, что может быть просто не эффективно. Становится дороже передавать внутренности объекта в функции, чем просто ссылку на этот объект, и получится пессимизация, а не оптимизация.
Однако здесь тоже можно кое-что сделать. Если мы замечаем, что наш объект действительно используется локально, никуда не утекает, кроме передачи вызова какой-нибудь функции и передачи его туда в качестве аргумента, где он тоже хорош и не утекает, то мы можем заменить аллокацию в хипе такого объекта на аллокацию на стеке.
Во фрейме метода foo() распишем этот объект: у него будет заголовок и поля. С ним немного по-другому будет работать сборщик мусора, он не будет пытаться его собрать, хотя обработка все равно нужна. В результате, с одной стороны, мы сможем передавать указатель на него внутрь новых функций, а с другой стороны, как только мы выйдем из foo , фрейм счеркнется, и мы моментально соберем память из-под него. Это тоже очень мощная оптимизация.
И наконец есть совсем плохой случай:
class Test < int a, b; >static Test t; public void foo(boolean shouldEscape) < Test obj = new Test(); obj.a = 42; obj.b = 10; System.out.println(obj.a + obj.b); if (shouldEscape) < t = obj; >>
Допустим, у меня есть метод, в котором объект obj в основном локален, но есть один неприятный холодный путь исполнения под условием shouldEscape , в котором все-таки происходит утекание. Я в глобальное поле t записываю ссылку на этот объект. С точки зрения любого escape-анализа здесь будет утекание. Здесь нельзя сделать ни scalar replacement, ни stack allocation. Но с другой стороны, мы же видим, что это просто один путь, всего лишь одна точка, где что-то пошло не так.
Есть анализы помощнее: partial escape analysis, который не просто скажет, что есть утекание (бинарный ответ, да или нет), а скажет: вот здесь объект используется локально, а в этих точках он действительно утекает. И тогда можно взорвать объект частично. Глобально не будет никакой аллокации, в самом начале мы начинаем работать с полями, а ровно перед точкой утекания мы эвакуируем наш объект в хип. То есть создаем новый объект, записываем его поля и присваиваем его куда нужно.
Такая оптимизация есть, например, в компиляторе Graal. Она очень эффективна на примере кода стримов или на примере Scala-кода, то есть именно на этих бенчах Graal себя очень хорошо чувствует. Во многом благодаря оптимизации Partial Escape Analysis.
Что получается: удаление new — это обратная реальная история от неявных исключений. Компилятор делает всё, чтобы рантайму ничего не нужно было делать. Многие new — тоже бесплатные, можно не бояться их ставить в своем коде, JVM постарается их убрать.
Scalar replacement — очень популярная оптимизация, она есть в HotSpot и не только. Partial Escape Analysis чуть более редкая штука, но встречается в Graal, во многих других современных виртуальных машинах тоже.
Stack allocation — более редкая оптимизация. Ее нет в HotSpot, зато она есть в IBM OpenJ9. Для HotSpot есть только proposal и прототип, который сделали люди из IBM. Они показываю, что это можно сделать, что это даст бенефиты на бенчмарках. Но пока кажется, что всё осталось на уровне предложения.
Это были какие-то конкретные примеры. Если говорить совсем глобально, то кроме того, чтобы компилятор сгенерировал вам корректный код, а рантайм его поддержал, они оба (чаще совместно) работают над оптимизацией производительности и потребления памяти вашей программы. И в этом взаимодействии, в этих оптимизациях и кроется львиная доля задач, которыми занимаются JVM-инженеры.
Если вы хотите узнать побольше примеров таких задач, то я очень советую почитать замечательный цикл статей Алексея Шипилёва. Они называются JVM Anatomy Quarks. В целом эти статьи построены как ответы на вопрос со стороны «а как в JVM что-то работает?», но по факту они так же описывают конкретные маленькие задачи, которые в этой JVM нужно сделать нам, JVM-инженерам. Если вам интересно посмотреть еще примеры наших повседневных задач, почитайте обязательно.
Обсудили, что делают JVM-инженеры. Теперь давайте поговорим, как они это делают.
Как разрабатывать JVM?
На каком языке программирования вы бы писали рантаймы и компиляторы?
С++ часто выбирается в качестве языка, который используется для разработки рантаймов компиляторов. Причин две:
Причина 1: производительность. То, что вы напишете, будет очень быстро работать.
Причина 2: связь с низким уровнем. Мы же вроде пишем внутренность языка, наверное, это должно как-то взаимодействовать с байтиками, с операционной системой и так далее.
Но не всё так однозначно!
Разбиваем вопрос на два: на чем писать компилятор и на чем писать рантайм.
На чем писать компилятор?
С компилятором всё довольно интересно. Задача компилятора — породить машинный код, корректный, эффективный, под нужную платформу.
Если совсем упростить задачу, то мы берем байты на вход (исходную программу) и генерируем байты на выходе (машинный код). И где тут связь с низким уровнем? Ничего такого нет, вам просто нужно уметь генерировать байты. В общем-то с этим справится любой язык, где есть какой-нибудь байт-буфер. Например, наша любимая Java. Так что обозначенная выше причина номер два здесь теряет смысл. Остается причина номер один, производительность.
Здесь тоже не всё так однозначно. Насколько быстро нам нужно скомпилировать этот код? Зависит от компилятора. Если у вас AOT-компилятор, то кажется, что у вас есть вообще всё время мира. Вы же заранее компилируете, вы никак не конкурируете с программой. Хоть 8 часов компилируйте, применяйте все оптимизации, которые хотите, всё будет замечательно. Конечно, наши клиенты почему-то со мной не согласны и спрашивают, а почему их Spring Boot-приложение компилируется 8 часов. Но на самом деле так-то ограничений у вас никаких нет. Вы не конкурируете за ресурсы с приложением.
С JIT-компиляторами всё немного хуже, потому что они работают параллельно с приложением. Если они будут это делать слишком долго, то стартап замедлится, что зачастую неприемлемо. Давайте посмотрим на реальные примеры, на разные компиляторы, режим их работы и на чем они написаны.
Начнем с «ненастоящего» компилятора javac .

Это AOT- или JIT-компилятор? Если вы ответили AOT, то в целом правильно, в основном он используется заранее, его можно дернуть в рантайме и что-то покомпилировать, но в целом да, это AOT. И он написан на чистой Java. И так обстоят дела уже очень давно, лишь самые первые версии javac были написаны на C, по понятным причинам. Конечно, можно сказать, что там мало оптимизаций, но на практике он и правда может работать довольно долго, и в целом это никого особо не напрягает. Это такой интересный знак.
Посмотрим на компиляторы в боевых реализациях виртуальных машин. Самые известные в HotSpot, C1/C2 — это JIT-компиляторы, написанные на С++. В IBM их OpenJ9 — тоже JIT, но может может использоваться как AOT, и он тоже написан на С++. Но во многом это сделано по историческим причинам. В тот момент, когда их делали, никому в голову не могла прийти идея начать писать на джаве. Тогда казалось очевидным, что если производительность, то С++.
А вот если посмотреть на что-то современное, например, на тот же Graal-компилятор — он тоже используется как JIT и как AOT, и он написан на чистой джаве. И никого это не беспокоит.
В Excelsior у нас в основном был AOT-компилятор, но был еще дополнительный JIT. И первый был написан вообще на Scala. Вдумайтесь — на Scala, на языке, который навернут над джавой. То есть там всё должно было работать еще медленнее. Но для написания оптимизирующего AOT-компилятора Scala идеально подходит. JIT-компилятор был написан на джаве, но опять-таки он вполне себе нормально работал.

Так что здесь С++ вообще не обязателен. Можно писать на каких-нибудь managed-языках.
На чем писать рантайм?
У нас снова те же проблемы: производительность и связь с низким уровнем. И вот в рантайме вы от этого не уйдете. Рантайм всегда работает параллельно с приложением. Его нельзя замедлять, он должен быть очень быстрым. Чем он будет быстрее, тем лучше. И связь с низким уровнем в рантайме как раз во все поля, потому что именно рантайм и позволяет связываться с операционной системой. Я вам показывал, как ловятся сегфолты — это как раз связь с операционной системой. И здесь просто необходимо иметь связь с низким уровнем. Более того, нам еще нужна предсказуемость и непрерывность исполнения и полный контроль над ним.
Представьте, что вы написали на джаве сборщик мусора. Вот он работает во время работы приложения. И сам он тоже останавливается для того, чтобы пособирать мусор. Это наверняка будет неожиданно, и паузы могут получиться ну совсем неприятные.
Значит, идем к С++? Не стоит торопиться. Давайте сначала посмотрим, а что в проекте Graal? В нем есть интересная подпапка substratevm . Про нее очень скромно написано, что это фреймворк для AOT-компиляции с Native Image. На самом деле это полноценный рантайм для Native Image. На чем же он написан?
Заходим в папку и видим GC.java . Получается, что как раз-таки сборщик мусора написан на джаве. Это интерфейс, у него есть реализация. И в одной из его реализаций есть такой интересный метод collectWithoutAllocating :

Это самое настоящее тело сборщика мусора, где внутри всё и происходит. А еще на нем есть интересные аннотации. Во-первых, @Uninterruptable — дескать, его нельзя прерывать. А во-вторых, @RestrictHeapAccess — нельзя аллоцировать. Согласитесь, будет странно, если ваш сборщик мусора начнет аллоцировать память. Вроде бы он должен делать всё наоборот. Соответственно, аннотация @RestrictHeapAccess как раз и говорит, что никаких new там быть не должно.
Эти аннотации обрабатываются компилятором, который тоже под контролем во всем проекте GraalVM. В результате вам гарантируется, что там всё будет замечательно. Вы как бы пишете на джаве, но при этом джавовский код получается хорошим.
А вот что про Substrate VM говорят сами оракловцы:

То есть это джава с некоторыми ограничениями. Но на это можно посмотреть и по-другому. Например, в самой джаве вы же не можете сказать, что вот этот метод должен быть без аллокации, а в Substrate VM вам такую фичу дают. То есть в каком-то смысле это расширение джавы для того, чтобы писать вот такой системный код.
В итоге на чистой джаве рантайм не напишешь, но можно делать свои специализированные полу-managed языки. Пример — как раз Substrate VM / SystemJava.
Что в итоге
Глобально получается так: что рантаймы, что компиляторы в современных виртуальных машинах можно писать на managed-языках (Java, Scala, их вариации для рантайма). И это дает нам много интересных плюшек. Например, разработка в IntelliJ IDEA, юнит-тесты, которые вы можете запускать просто на JUnit, всякие code coverage, более низкий порог входа в разработку и так далее. То есть не так страшен черт. Современная разработка виртуальных машин может быть довольно близка к обычной джава-разработке.
Отлаживай так, словно никто не видит
С разработкой разобрались, а что с отладкой?
Допустим, вы совершили какую-нибудь ошибку. Например, в коде сборщика мусора. Что это значит?

После этого могут начать собираться живые объекты, и это очень неприятно. Это значит, что у вас могут происходить развалы виртуальной машины (чего вообще не должно никогда происходить). Но это полбеды.
Гораздо хуже, когда у вас начнет происходить некорректное поведение. Например, вы пытаетесь породить какую-нибудь пдфку программой на джаве, а у вас вместо этого выводятся всякие каракули. Если какие-то объекты оказались побитыми сборщиком мусора, такое вполне может произойти.
Может быть и обратная ситуация. Допустим, вы ошиблись так, что вы не собираете мертвые объекты. Формально, это не баг, но это будут утечки памяти, и ваши клиенты вам за это спасибо не скажут.
Ладно, скажете вы, все мы люди, подумаешь, ошиблись. Давайте возьмем и отладим это. А как отладить? Зачастую такие проблемы проявляются раз в N запусков, где N может быть ~10000. Самый долгий баг, который я отлаживал, проявлялся раз в год. При этом каждую ночь шло нагрузочное тестирование на десятки тысяч тестов. За две попытки я его вроде бы починил. Может быть еще через пару лет он снова выстрелит, но пока прошел третий год, ничего не упало — я считаю, что баг починен.
Так что если вы попытаетесь что-то проявить в отладчике, то шансы у вас довольно несерьезные. Более того, отладчик вам еще и тайминги побьет, и в результате у вас точно ничего хорошего не получится. Вы можете сказать: а давайте старым дедовским способом будем вставлять логи. Но логи тоже не помогут — диска не хватит.
Допустим, вы хотите логировать аллокацию каждого объекта или очистку памяти из-под каждого объекта. Объектов миллиарды, они создаются, пропадают, прокачка памяти может достигать сотен гигабайт или уже даже терабайт. У вас никакого диска не хватит. Вы просто не сможете записать эти логи, а потом не сможете их обработать. Поэтому здесь тоже не получится ничего хорошего. Что с этим делать?
В качестве отладки здесь работает измененная психика рантаймщика. Все рантаймщики — ужасные параноики. Если посмотреть на код любого рантайма любой виртуальной машины, он весь покрыт ассертами, иногда, казалось бы, совсем очевидными. Но они и правда нужны, потому что если вдруг по какой-то причине какой-то инвариант разломается, то самым неожиданным образом ваша виртуальная машина тоже может разломаться. Так что — куча ассертов.
Если вдруг что-то все-таки ломается, мы оставляем ловушки. «Если вдруг что-то пошло не так, давай вот здесь не просто ассерт упадет, а пусть у нас будет огромная печать: мы не просто сохраним хипдамп, мы еще вычислим много всего и распечатаем это всё в отдельный специальный файл». А потом мы терпеливо, несколько недель или месяцев, ждем, пока этот развал случится, и жадно читаем полученный лог.
Есть еще один инструмент — формальная верификация. Иногда это звучит как магия, но современная наука уже довольно хорошо продвинулась к тому, чтобы математически доказывать, что у вас нет никаких багов в вашем коде. И рантаймы, и компиляторы — это область, в которой очень хочется строить формальные модели и ими доказывать, что всё в порядке. Некоторые части нашего рантайма мы вот таким образом формально верифицировали. И были более-менее спокойны, что там нет никаких ошибок.
Когда вы ошибаетесь, это еще полбеды. Бывают гораздо более странные случаи, которые вызывают самый настоящий эффект бабочки.
Эффект бабочки
В Excelsior мы, JVM-инженеры, сами сидели на саппорте. У каждого была особая смена, когда ты откладываешь все дела и садишься отвечать клиентам. Если у них что-то ломается, ты пытаешься им помочь и всё отладить, разобраться, что пошло не так.
И вот сижу я как-то на саппорте, мне пишет клиент. Говорит, знаете, у меня на вашей JVM джава-приложение разваливается, вот крэш-лог. Смотрю крэш-лог, он указывает куда-то в дебри msvcr100.dll (это происходит на винде), поэтому выходит, что это где-то в нативном коде что-то сломалось, а не у нас. Конечно же на нашей стороне ничего не воспроизводится, такие вещи вообще редко когда воспроизводятся. Я уже готовлюсь писать плохое письмо и говорить, что дело скорее всего не в нас, и как вам помочь — непонятно. Но в последний момент решают погуглить лог такого развала. И оказывается, что у очень многих людей на разных виртуальных машинах всё разваливается одинаково, всё точно так же уходит в нативный код, в один и тот же метод в msvcr100.dll . И всех людей, у кого это разваливается, есть еще кое-что общее: Windows 10 Creators Update и вот такая папка на рабочем столе:

Это какой-то хитрый бэкдор в винде. Если создадите такую папку, то нажимая на нее, вы получаете контрольную панель, шорткаты к настройкам винды. Называется это «Папка Бога», на ютубе есть куча роликов про то, как ее создать и использовать (кто бы мог подумать, что кому-то это может быть интересно и полезно).
И у всех, кто жалуются на развалы, оказывается такая папка на рабочем столе. Я спрашиваю у нашего клиента: «А у вас совершенно случайно на рабочем столе нет папки GODMODE …?» Я представляю, что он подумал, это было как раз в разгар историй про русских хакеров, подумал небось, что мы за ним следим. Еще больше он удивился, когда я попросил удалить эту папку, и после этого у него всё заработало.
Что это было? В Windows 10 Creators Update изменилось поведение системного вызова IShellFolder::GetDisplayNameOf(. ) . При некоторых входных данных она начала возвращать NULL , а раньше такого не было. В JDK есть какое-то количество нативных методов на С, которые дергают этот системный вызов, не проверяя результат. Вот и получается: поменяли системный вызов, поведение всех нативов изменилось, и в результате что у нас, что на HotSpot, всё начало взрываться. При этом проявлялось это далеко не у всех пользователей: нужно было, чтобы звезды сошлись — правильное приложение, правильная версия Windows, папка бога на рабочем столе… Вот такая комбинация вызывала случайные развалы.
Может показаться, что это какая-то очень странная байка, но на самом деле в разработке JVM похожие истории случаются часто. Если вы что-то изменили, вдруг может пойти эффект бабочки совсем в другой части виртуальной машины. Или даже какой-то другой писатель операционной системы что-то у себя изменил, и всё может начать ооочень странно разваливаться именно в вашем коде.
В общем, отладка багов в JVM — это интересная детективная история. Обычно когда ты ее разматываешь, ты под конец как детектив восклицаешь: «Так вот кто убийца!». Это классное ощущение. Но в процессе придется, конечно, помучаться. Отдельно доставляет, что на поведение сильно влияют операционная система и железо. И зачастую, если в вашем джава-приложении начинаются случайные крэши, стоит проверить железо. Вы как пользователь можете запустить memtest и проверить, не побилась ли у вас память. Потому что настолько плотно и хорошо расставлены ассерты в рантайме, что если что-то ломается — это абсолютно невозможная история, и скорее всего что-то пошло не так с железом. И здесь как раз парадоксальным образом использование managed-языков программирования вроде джавы добавляет вам безопасности при разработке компиляторов и рантаймов.
Клиенты — Java-программисты
Еще один аспект нашей работы — это тот факт, что наши клиенты — это Java-программисты. В целом это очень круто. Любой разработчик тулов меня поймет: когда твои клиенты — тоже программисты, вы с ними на одной волне, у вас общая культура. Если у тебя что-то разваливается, они тебя прощают, потому что думают — ну это ж тоже программист, понятно, что у них там что-то может не работать.

В общем, приятная история. До тех пор, пока эти программисты не лезут в sun.misc.Unsafe . Потому что после этого начинаются очередные ужасы.
История из жизни. Какое-то количество клиентов стали репортить нам, что они используют библиотеку Kryo, и у них внезапно полезли вот такие стек трейсы:

Исключительно на нашей виртуальной машине. На HotSpot это не проявляется. Значит, мы что-то сломали. Идем разбираться. Стек-трейсы идут вот из такого кода:
offset = unsafe().objectFieldOffset(f); … public Object getField(Object object) throws IllegalArgumentException, IllegalAccessException < if (offset >=0) < return unsafe().getObject(object, offset); >else throw new KryoException("Unknown offset"); >
Kryo же библиотека для сериализации/десериализации, так что логично, что она работает с офсетами. Получили офсет-поле через objectFieldOffset(f) из unsafe() . Потом берем getField() и проверяем: смотрите, офсет, который нам сюда передали, обязательно больше или равен 0, иначе выкидываем ошибку. Звучит чертовски логично, какие офсеты могут быть отрицательными? Это же действительно странно. Офсеты должны начинаться с нуля.
Но на самом деле, если посмотреть на метод objectFieldOffset() , там английским по белому написано:
/** * Reports the location of a given field in the storage * allocation of its class. Do not expect to perform any * sort of arithmetic on this offset; it is just a cookie * which is passed to the unsafe heap memory accessors. * . * */ @ForceInline public long objectFieldOffset(Field f)
Эти штуки — это не офсеты. Не важно, что они так называются, это просто какие-то очень специальные значения, куки, вы не можете проводить с ними никакие арифметические операции. Даже сравнивать их не можете, это не числа, это просто какие-то волшебные штуки.
У нас они получились отрицательными. Вопрос, почему? Всё очень просто. Если посмотреть на офсеты, то это long . long — это 64 бита. Таких длинных офсетов не бывает, не нужно столько места. Так вышло, что это long , но бывает. Обычно используются какие-то нижние биты. Вот у меня офсет 12, 4 бита я использовал, еще 60 свободно.

И кажется неплохой идеей взять старшие биты, которые никогда не будут заняты, и что-то туда сложить. В нашем случае мы туда складывали дополнительную информацию о том, было ли это поле интерфейсного типа или нет, и если да, еще немного мета-информации. Имеем право. Эта информация нужна только для unsafe , мы ее только там и обрабатывали, и мы ее использовали, чтобы unsafe работал быстрее. Мы его разгоняли по сравнению с тем, как он работает в HotSpot. Казалось бы, делали доброе дело.
Но есть небольшая проблема: если в верхние биты офсета что-нибудь записать, то это будет уже не 12, это будет (long) offset == -6917529027641081844 . Ну и если вы начинаете дословно проверять это значение на >=0, то вы увидите здоровое отрицательное число. Правка неправильная. К счастью, это опенсорс, так что мы отправили пулл-реквест, что надо проверять на специальную константу, которая заведена в этом unsafe :
public Object getField(Object object) throws IllegalArgumantException, IllegalAccessException < if (offset != Unsafe.INVALID_FIELD_OFFSET) < return unsafe().getObject(object, offset); >else throw new KryoException("Unknown offset") >
И только если оказалось, что это равно специальной константе, тогда выбрасывай исключение, иначе доставай объект. Я думал, что такой пулл-реквест никогда не примут (там был довольно длинный текст, почему нельзя пользоваться unsafe в таком контексте). Думал, нас пошлют, но мейнтейнер моментально всё замержил и поблагодарил. Это было очень приятно. Это как раз тот момент, когда чувствуешь: клиенты — Java-разработчики. Это очень классно.
Ну а глобально вывод такой: приватные API лучше не использовать. Понятно, что всем надо, очень важно взять unsafe и что-то там поделать. Если вы это делаете, то будьте готовы в развалам и плохой работе с кастомными JVM. Речь даже не о том, что вы берете какую-то другую JVM. Вы можете просто обновиться до следующей версии джавы, внутреннее поведение с unsafe изменится, и в результате у вас всё может пойти как-то по-другому. Если вы хотите быть абсолютно уверенным, идите в исходники своей виртуальной машины и проверяйте, например, что офсеты у вас обрабатываются так, как вы думаете. Тогда можно делать такие правки, но опять-таки, под кучей комментов и объяснений.
Чтобы у вас не было ощущения, что я ругаю джава-разработчиков и говорю, какие JVM-разработчики молодцы, вот вам история, как JVM-разработчики всё сломали.
Почему JVM-разработчики так долго всё делают
В джаве есть интересная фича: на любом объекте можно синхронизироваться.
synchronized (obj)
Фича сомнительная, многие языки ее не поддерживают, только на каких-то специальных объектах. А в джаве можно синхронизироваться на всем подряд. Ну а раз она есть, нужно ее как-то реализовывать. Самое базовое и понятное решение, которое есть — это когда мы у себя пришли в synchronized и создаем объекту зеркальный объект, который будет представлять из себя нативный монитор. И уже на нем мы будем синхронизироваться, через средства операционной системы засыпать или потом просыпаться и так далее.

Хорошее решение, правда, работает довольно медленно. Более того, генерация новых объектов — это очень неприятно, нужно потом думать, куда их деть и так далее. Поэтому стали думать над оптимизациями, как сделать это лучше.
Первая идея для оптимизации: чаще всего, даже если вы пишете synchronized , никакой конкуренции нет. У вас всё равно в один момент времени только один тред приходит и захватывает объект. Исходя из этой идеи сделали первую оптимизацию, которая называлась Thin lock: вместо настоящего объекта с нативным монитором просто на стеке аллоцировали объект с Thin lock, с помощью которого через обычные CAS-ы происходила синхронизация, что уже гораздо быстрее.

Если вдруг contention случался, то тогда можно откатиться к предыдущей схеме. Но и этого недостаточно. Есть еще одна идея для оптимизации.
Вторая идея для оптимизации: зачастую вообще только один тред входит в synchronized block . Ну просто код так написан, что метод synchronized , а никакого многопоточного кода нет, всё происходит локально. И в таком случае можно пойти дальше и уже даже не делать thin lock, а просто внутрь объекта, в его заголовок записать id треда при первом посещении synchronized , который туда и будет приходить. Тогда первый synchronized будет чуть дороже, зато во всех остальных даже CAS делать не нужно будет, мы сможем просто обычной проверкой узнавать, захватили мы его или нет. Это знаменитая оптимизация Biased Locking , которая реализована буквально во всех виртуальных машинах и даже в HotSpot, но до Java 19.

Звучит очень круто и в общем-то просто. Но есть нюанс… Если вы посмотрите внутрь кода того же HotSpot, то там будет нарисована примерно вот такая схема, в каких стейтах бывает объект относительно этого bias:

Обратите внимание, что на схеме сказано simplified, то есть это упрощенная схема, которая на самом деле выглядит немного сложнее. А теперь представьте, что вы рантайм-инженер и вам нужно сделать новую фичу, которая явно сильно трогает трединг и синхронизацию. Например, в проекте Loom вы хотите сделать новые виртуальные треды, которые работают явно как-то по-другому, в том числе в синхронизации. Берем эту схему и умножаем на два. Получается еще более монструозная история.
Это сильно ухудшало жизнь JVM-разработчикам. Более того, были и до сих пор остаются контр-примеры среди пользовательского кода, которым Biased Locking только мешает. Например, в Одноклассниках на Cassandra всегда был отключен Biased Locking , потому что конкретно в случае Кассандры это просто пессимизация, ухудшение производительности.
Что с этим сделать? Конечно же удалить! В JDK 15 Biased Locking задепрекейтили и отключили, в JDK 19 удалили. Всё замечательно, разделались с техническим долгом, у пользователей всё ускорилось, все должны быть довольны, разработчики могут делать новые крутые фичи.
Правильно? Нет. Пару недель назад в мейлинг лист написал Эндрю Динн из RedHat и говорит: «У нас клиенты жалуются, что у них вот такой, казалось бы, очень простой код ни с того, ни с сего просел на новой джаве».
boolean contentChanged = false; BufferedInputStream oldContent = . ; BufferedInputStream newContent = . ; try < int newByte = newContent.read(); int oldByte = oldContent.read(); while (newByte != =1 && oldByte != -1 && newByte == oldByte) < newByte = newContent.read(); oldByte = oldContent.read(); >contentChanged = newByte != oldByte; > catch (IOException e) < contentChanged = true; >.
Что делает этот код: у него есть два BufferedInputStream , мы по очереди читаем из них байтики и сравниваем их. И внезапно почему-то на Java 19 всё стало как-то медленно работать. Потому что метод read — synchronized , в коде контеншена нет, мы всё делаем локально, но без Biased Locking оно просто просело:
public synchronized int read() throws IOException < if (pos >= count) < fill(); if (pos >= count) return -1; > return getBufIfOpen()[pos++] & 0xff; >
Как выглядели бенчмарки? У нас просто есть readOut , который берет какой-то InputStream и из него побайтово что-то читает. И есть два бенчмарка. Первый — это мы просто передаем FileInputStream :
private void readOut(Blackhole bh, InputStream s) throws IOException < int b; while ((b = s.read()) != -1) < bh.consume(b); >> @Benchmark public void fis(Blackhole bh) throws IOException < try (InputStream s = new new FileInputStream(file)) < readOut(bh, s); >>
А второй — мы туда передаем BufferedInputStream :
@Benchmark public void fis_bis(Blackhole bh) throws IOException < try (InputStream s = new BufferedInputStream(new FileInputStream(file))) < readOut(bh, s); >>
Сравниваем и выясняем, что версия с BufferedInputStream , которая по идее должна быть быстрой (она правда быстрая), просела в Java 19 больше чем в два раза. Просто с ходу код, который 20-25 лет работал замечательно, стал работать в два раза медленнее. И с этим абсолютно ничего нельзя сделать, кроме того чтобы переписать его на какие-то более современные средства.
И получилось, что тут была вилка: слишком сложный код в рантайме, который трудно поддерживать, или клиенты, у которых теперь тормозит код (причем у «молчаливых» клиентов, у которых все всегда было замечательно). Хорошего решения нет, но удаление biased locking выходит боком: так как у вас даже нет ручки, чтобы его включить или выключить, вы подставляете пользователя, который раньше этим совершенно спокойно пользовался.
В других виртуальных машинах biased locking остался, и зачастую он сделан проще. Схема, о которой я чуть раньше рассказывал, у них тоже довольно монструозная, но все же попроще, так что поддерживать biased locking проще, и может быть это хороший путь, по которому и стоило пойти. Но однозначного ответа тут нет.
Что в итоге
Разработка JVM — это вызов, научный и инженерный. Научный, потому что вы расширяете научное знание, чтобы придумать какие-то новые оптимизации в компиляторе или алгоритме сборщика мусора, то есть то, что еще никто вообще в мире не придумывал. Инженерный, потому что, например, в обработке неявных исключений ничего супернаучного нет, но это крутой инженерный трюк, который тоже очень интересно проделывать.
Разработка JVM — это еще и детектив. Когда у вас что-то разваливается и вы очень долго это отлаживаете, это буквально как будто вы читаете томик про Шерлока Холмса, где вы и Холмс, и Ватсон и зачастую Мориарти.
И еще это общение с клиентами Java-программистами, что обычно очень здорово, пока они не забредают в sun.misc.Unsafe .
Наконец, несмотря на все те ужасы, которые мы раз за разом рассказываем вам на джава-конференциях, люди продолжают спрашивать меня: вот я джавист, можно мне стать JVM-инженером? Или гномом можно только родиться?
Конечно, можно. Я знаю довольно много примеров таких людей, которые начинали как джависты и потом в какой-то момент им нужно было всё глубже и глубже опускаться в дебри, и они в конце концов превращались в гномов. Если вы вдруг задумываетесь об этом, пожалуйста, начните с чтения статей Алексея Шипилёва. Если вам нравится, что там написано, если вам кажется, что это классный вызов, то это хороший признак.
Сам HotSpot уже давно на гитхабе, и несмотря на то, что это плюсовый код, вы вполне можете пойти посмотреть на эти сорцы и понять, нравится вам с таким работать или нет. Ну и более милые современные проекты вроде Graal тоже в открытом доступе. Имея джава-бэкграунд, можно вполне начать разбираться с тем, что там происходит. А потом можно взять какой-нибудь issue, разобраться и решить. Например, вот что добавилось в ноябре в проекте Graal:

Вместе с OOM безусловно создается core dump. Наверное, это неправильно, нужно исправить!
Или сделайте что-нибудь в грааль-компиляторе. Дел очень много. Приходите и пробуйте. Это прекрасное опенсорсное комьюнити, и у вас всё получится.
От организаторов конференции: это был доклад с нашей осенней Java-конференции, а мы тем временем вовсю готовим весеннюю — в апреле проведём JPoint 2023. Так что, если этот пост был вам интересен, обратите внимание — на сайте уже есть описания многих новых докладов. 12-13 апреля пройдёт онлайн-часть, а 18-19 апреля можно будет хоть лично собраться в Москве на офлайн-часть, хоть подключиться и к ней удалённо.
- Блог компании JUG Ru Group
- Java
- C++
- Системное программирование
- Компиляторы
