Команда Spring АйО перевела и адаптировала доклад Брайана Гоетца «Valhalla — эпичный рефакторинг Java», который будет опубликован несколькими частями. В первой части серии будет рассказано об истории и причинах появления проекта Valhalla и, вкратце, об основных целях, которые ставила перед собой команда.


Что такое проект Valhalla?

Этот доклад посвящён прогрессу проекта Valhalla с момента последнего обновления. Мы разберём его цели, ключевые идеи и ответим на главный вопрос: зачем вообще понадобился этот проект?

Кто-то метко назвал Valhalla «Java»s Epic Refactor», и это прозвище прижилось. Проект меняет саму суть взаимодействия языка с памятью, затрагивая не только синтаксис, но и рантайм, библиотеки и инфраструктуру. Работа над ним идет уже 10 лет, и хотя ещё не всё завершено, команда проекта видит финишную черту.

За это время было создано множество прототипов и проведено бесчисленное количество экспериментов. Если раньше фокус был на взаимодействии с аппаратным обеспечением, то теперь команда сосредоточена на том, как Valhalla повлияет на разработчиков. Главная цель — сделать нововведения естественной частью Java, без ощущения «чужеродности». Причём команда осознанно избегает введения новых ключевых слов, связанных с такими понятиями, как структура памяти (memory layout) и сглаживание (flattening) — этот путь уже приводил другие языки к неудаче.

Valhalla — это не просто эксперимент, а глубокая эволюция Java. И, судя по текущему прогрессу, финальный результат будет стоить всех затраченных усилий.

Главная задача состоит в том, чтобы найти правильный способ семантического обрамления желаний программиста, чтобы JVM могла предоставить желаемую оптимизацию. Но сначала о целях проекта.

Структуризация памяти

JVM работает с двумя видами данных: примитивными значениями и ссылками на объекты. Если заглянуть в спецификацию, там ничего не сказано о том, как именно объекты размещаются в памяти. Но если внимательно вчитаться, становится очевидно: у JVM просто нет выбора.

Семантика Java фактически вынуждает JVM хранить объекты так, как это объясняют на уроках информатики — с блоками памяти и указателями. В итоге ссылки превращаются в своего рода указатели, а объекты — в блоки памяти.

Оптимизации, например Escape Analysis, в некоторых случаях помогают избежать лишних аллокаций. Но рассчитывать на них как на универсальное решение не стоит — слишком много нюансов.

Таким образом, если есть 2 класса: Point (точка) и Rect (прямоугольник), внутри которого есть еще две точки, и мы просим программу создать прямоугольник, получится модель структуры памяти, которая выглядит, как та самая, знакомая по урокам информатики диаграмма. 

record Point(int x, int y) { }
record Rect(Point p1, Point p2)  { }

Rect r = new Rect(new Point(1, 2),
			  new Point(3, 4));

Итак, у нас есть указатель на объект Rect, у которого есть заголовок и два указателя, каждый из которых указывает на другой объект, тоже с заголовком. Эти последние два объекта содержат в себе цифровую рабочую нагрузку как таковую.   

И в этой модели нет ничего плохого, она имеет право на существование, но для некоторых приложений она может быть не так производительна, как хотелось бы. На самом деле эта модель недостаточно плоская (flat), то есть у нас есть небольшие фрагменты данных, соединенные указателями. В то же время она также недостаточно плотная (dense), потому что большая часть памяти, используемой в этой структуре, занята заголовками объектов и указателями, а не самими данными.

И в последние 30 лет относительная стоимость операций с памятью стала сильно отличаться от стоимости вычислительных операций. Около 30-40 лет назад операция fetch, которая работает с памятью, и арифметическая операция стоили примерно одинаково. Операции над памятью стали дороже, арифметические операции стали быстрее, и в результате модели стоимости разошлись в разные стороны. 

Если у вас неоднородная (non-flat) структура памяти, это может привести к Cache Miss-ам, а они дорого стоят. Поэтому вопрос, который задает Valhalla, следующий: при каких условиях JVM сможет структурировать память по такой прямоугольной схеме вместо той модели, которая была показана выше?

Потому что во многих случаях именно такая модель является предпочтительной.

Массивы

В случае использования массивов ситуация становится еще хуже.

У нас есть массив объектов-указателей, где каждый указатель является объектом,  и чтобы добраться до значения под индексом, я должен пройти по ссылке, что может привести к Cache Miss-у. Таким образом, если мы пройдем по всему массиву вниз, прочитаем значения x и y и затем перейдем далее, мы скорее всего потеряем все преимущества от cache locality и hardware prefetch. 

Поэтому довольно часто мы хотим получить структуру памяти, которая выглядит вот так:

Вопрос в том, как сделать это возможным. Какой код мы должны попросить разработчиков писать, чтобы такая структура стала возможной? И более глобальный вопрос — можно ли сделать это, не превращая Java во что-то такое, чем она быть не хочет, а именно, некое подобие C++?

Примитивные типы и объекты

Как ни странно, эта проблема напрямую связана с примитивными типами. В Java с самого начала существовала пропасть между примитивами и объектами — можно даже назвать это первородным грехом языка.

В 1995 году такое разделение было вынужденным: JIT-компиляторов ещё не существовало, и если бы все числовые типы сразу были объектами, производительность арифметических операций оказалась бы неприемлемой. Но за этот компромисс Java платит до сих пор.

Разделение создаёт массу неудобств. Вы наверняка это ощущали, например, когда приходилось создавать ArrayList<Integer> вместо ArrayList<int>. Это кажется неестественным, и интуиция подсказывает: «должен быть более удобный способ».

Проблема ещё и в том, что это разделение пронизывает все уровни языка. Когда в Java появились дженерики, они унаследовали этот компромисс: дженерики работают со всеми типами, кроме тех самых восьми, которые используются чаще всего. В результате ArrayList<int> не может хранить int[] — приходится заворачивать числа в Integer[].

И структура памяти для этого случая выглядит точно так же, как приведенный выше рисунок для массива. 

А потом стало ещё хуже. В Java 8 произошёл настоящий взрыв — появление специализированных функциональных интерфейсов и классов вроде IntStream и IntToLongFunction. Никто не хотел этого множества дополнительных типов, но оно стало неизбежным последствием компромисса, на который создатели языка пошли ещё в 1995 году. В итоге разработчики Java, забравшись на вершину этого стека решений, осознали, что ситуация зашла в тупик — так и зародился проект Valhalla.

Примитивные типы и объекты в Java различаются кардинально:

  • Примитивы встроены в язык, объекты задаются пользователем.

  • У объектов есть члены и супертипы, у примитивов — нет.

  • Объекты имеют identity, примитивы — нет.

  • Объекты nullable, примитивы — нет.

  • Примитивы можно использовать без конструктора, а объекты — только после его выполнения.

  • Объекты поддерживают полиморфизм, примитивы — нет.

Если представить возможности языка в 18-мерном пространстве, примитивы и объекты окажутся в противоположных его точках. Унифицировать их — серьезный вызов, но именно этим и занимается проект Valhalla.

В теории можно обойтись одними классами, но примитивные типы существуют не просто так — они быстрее. Их скорость обусловлена плотностью (dense) и плоскостью (flat), благодаря которым они занимают меньше памяти и обрабатываются эффективнее.

Но это ставит разработчиков перед неприятным выбором:

  1. Писать чистый, красивый, объектно-ориентированный код, но рисковать потерей производительности (или хотя бы думать, что теряешь).

  2. Использовать примитивные типы, жертвуя читаемостью и поддерживаемостью ради скорости.

И когда все вынуждены выбирать плохой вариант, стоит предложить лучший.

Есть ещё одна проблема: в Java всего восемь примитивных типов. Добавить девятый уже сложно, а сотню — вообще нереально. Но действительно ли эти восемь типов останутся единственно правильными на всю историю компьютерной науки? Конечно, нет.

Сегодня в Java нет встроенных аналогов для Float16, Complex или чисел с фиксированной точкой. Они вынуждены существовать в виде классов, потому что новые примитивные типы добавить невозможно. И Valhalla решает и эту проблему.

Предшественником проекта Valhalla стала работа Гая Стила от 1998-го года “Growing a Language”, и видео от него же, он представляет свое видение того, как мы можем достичь преимуществ встроенных числовых типов без того, чтобы делать их встроенными. Так что в каком-то смысле можно сказать, что проекту Valhalla уже почти 30 лет.

Цели проекта Valhalla

Если просто перечислить цели проекта Valhalla, получится следующий список:

  • Более плоская и плотная структура памяти 

  • Унификация объектов и примитивных типов 

  • Ликвидация пропасти между объектами и примитивными типами

  • Ввод новых числовых типов, добавляемых как библиотеки, а не встроенные типы, с сохранением тех же преимуществ рантайма, которые сегодня есть у примитивных типов 

Идея Valhalla — объединить лучшее из двух миров. Дать разработчикам гибкость классов и производительность примитивов. Главный принцип проекта можно выразить фразой:
«Кодируется как класс, работает как int».

Чтобы реализовать это, нужен «эпичный рефакторинг», но цель проста: всё должно выглядеть так, словно эти возможности были в Java всегда.

За 10 лет команда Valhalla шаг за шагом проделала огромную работу: эксперименты, прототипы, исследования — от языка и компилятора до рантайма, JIT и библиотек. Постепенно всё сложилось в цельную концепцию, но самым сложным оказалось не техническое решение, а его удобство для разработчиков.

Главный вызов — создать понятную и естественную модель, которая не выглядит искусственно прикрученной и позволяет плавно переходить от старого кода к новому, использующему value-классы.

Где-то пять-шесть лет назад у нашей команды было почти готовое решение, и казалось, что оно работает. Оно соответствовало всем пунктам требований, но попытка писать код, основанный на этом решении, приводила к ужасным результатам. Поэтому команда потратила еще несколько лет на выпиливание сложностей, убирание ненужных возможностей, и примерно шесть месяцев назад эта сложность действительно ушла.

Все начиналось с модели, которая имела новые bytecode инструкции, новые правила верификации, новые дескрипторы типов в виртуальной машине, новые формы пулов констант, но если почитать спецификацию JVM для JEP 401 сейчас, то там ничего этого нет. Есть немного нового по классам, немного нового по полям, новый атрибут для поля, и это все.


На этом заканчивается первая часть серии статей о проекте Valhalla по докладу Брайана Гоетца. В будущих публикациях мы расскажем о препятствиях, которые возникли у команды на пути к реализации проекта, о том, как команда эти препятствия преодолевала, а также о первых результатах, о которых можно говорить уже сегодня.

Комментарии (7)


  1. Dhwtj
    04.02.2025 13:00

    Примитивные типы те, что нативно обрабатываются процессором. Так что комплексные не станут нативными. Вот SSE и тому подобное можно бы ввести в виде расширений языка. В Си такое есть, про Java не знаю.


    1. WaitEvent
      04.02.2025 13:00

      есть. гугли 1BRC challenge


      1. Dhwtj
        04.02.2025 13:00

        А точнее?


  1. domix32
    04.02.2025 13:00

    Я то думал они хотят нативный AoT обеспечить.


    1. GerrAlt
      04.02.2025 13:00

      Это вроде как обеспечил GraalVM уже несколько лет назад


      1. domix32
        04.02.2025 13:00

        Не особо следил за их экосистемой, мог пропустить. Но в среднем довольно забавно наблюдать, как Zig повлиял на Java (спорное утверждение, доказывать я его конечно же не буду).


  1. Deosis
    04.02.2025 13:00

    То есть они хотят сделать аналог структур C# двадцатилетней давности?