Java-разработчики годами жили по принципу «Everything is an object». И всё бы хорошо, если бы за это не приходилось платить «налогом на объект». Каждый раз, когда вы создаёте простой класс из двух полей int, JVM бережно дописывает к нему тяжёлый заголовок, упаковывает в ссылку и разбрасывает по куче, заставляя процессор страдать от постоянных промахов кеша.

Мы привыкли к этому компромиссу. Мы научились использовать примитивные коллекции и костыли, чтобы выжать производительность там, где объектная модель Java начинает буксовать. Project Valhalla призван изменить правила игры.

«Стена памяти» и налог на Identity

В классической Java любой объект обладает идентичностью (Identity). Даже если у вас есть класс Point(int x, int y), для JVM это сложная сущность, которая должна поддерживать:

  1. Полиморфизм и ссылки: объект живёт в куче, а переменная хранит лишь адрес

  2. Блокировки (Locking): вы можете вызвать synchronized(obj), так как в заголовке объекта (Object Header) зарезервировано место под монитор

  3. Мутабельность: вы можете изменить поле, не меняя адрес объекта

Подвох в том, что заголовок объекта занимает 8—16 байтов. Если у вас массив из миллиона Point, то вы тратите ~16 МБ только на заголовки и ещё ~8 МБ на массив указателей. Полезные данные (координаты) теряются в этом океане служебной информации. Но хуже всего — Pointer Chasing. Процессор хочет прочитать координаты, но сначала читает адрес из массива, затем идёт в память по этому адресу (часто промахиваясь мимо кеша L1/L2), и только потом получает x и y.

Решение: JEP 401 и Value Classes

Project Valhalla — это масштабная инициатива по обновлению объектной модели Java. Её ключевым элементом являются Value-объекты, объединяющие абстракции объектно-ориентированного программирования с характеристиками производительности простых примитивов. По состоянию на январь 2026 г. проект активно развивается и доступен через ранние сборки JDK. Основой является JEP-401, который разделяет понятие объекта в Java на два типа: Value-объекты и Identity-объекты.

Основными целями проекта Valhalla являются:

  • Добавление модели программирования, в которой объекты различаются исключительно значениями своих полей, подобно тому, как int-значение 3 отличается от int-значения 4

  • Обеспечение плавной миграции существующих классов в Value-классы, например, Integer или LocalDate

  • Потенциальные оптимизации использования памяти и сборки мусора

Зачем нам Value-объекты

Часто разработчикам необходимо представить некоторый Domain-объект, который, например, хранит дату события. Обычно для этого создают неизменяемые классы, содержащие достаточно логики для создания и проверки объекта, а метод equals переопределяют так, чтобы сравнивались поля класса. То есть два объекта с одинаковыми значениями полей могут считаться взаимозаменяемыми, несмотря на то, что ссылки на эти объекты различаются.

Как пример, дата события может храниться в поле с типом LocalDate:

jshell> LocalDate d1 = LocalDate.of(1996, 1, 23)
d1 ==> 1996-01-23

jshell> LocalDate d2 = d1.plusYears(30)
d2 ==> 2026-01-23

jshell> LocalDate d3 = d2.minusYears(30)
d3 ==> 1996-01-23

jshell> d1.equals(d3)
$4 ==> true

В этом примере «сущностью» объекта LocalDate для разработчика будут значения года, месяца и дня, а для Java — его идентичность: область памяти, Object‑Header, Identity Hash Code. Даже если два объекта LocalDate хранят одинаковые даты, на самом деле это два разных объекта с разными идентичностями.

Для изменяемых объектов важна идентичность. Она позволяет различать два объекта, которые сейчас находятся в одинаковом состоянии, но в будущем могут измениться и принять другое состояние. Иными словами, если объекты изменяемы, они не взаимозаменяемы. Но большинство доменных значений не изменяются, а значит, могут считаться взаимозаменяемыми. На практике нет никакой разницы между двумя объектами LocalDate со значением 1996-01-23, поскольку их состояние фиксировано и не изменяется. Они, как и сейчас, будут представлять одно и то же доменное значение, поэтому нет необходимости различать их по идентификаторам. 

JDK пытается уменьшить путаницу для неизменяемых классов, моделирующих примитивные значения, такие как Integer. В частности, автоматическая упаковка небольших int-значений в Integer использует кеш, чтобы избежать создания Integer-объектов с уникальными идентичностями. Однако этот кеш не распространяется на четырёхзначные int-значения, такие как 1996:

jshell> Integer i = 96, j = 96;
i ==> 96
j ==> 96

jshell> i == j
$3 ==> true

jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996

jshell> x == y
$6 ==> false

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

Например, предположим, что программа создаёт массивы значений int и объектов LocalDate:

jshell> int[] ints = { 1996, 2006, 1996, 1, 23 }
ints ==> int[5] { 1996, 2006, 1996, 1, 23 }
jshell> LocalDate[] dates = { d1, d1, d2, null, d3 }
dates ==> LocalDate[5] { 1996-01-23, 1996-01-23, 2026-01-23,
                         null, 1996-01-23 }

Массив int может быть выделен виртуальной машиной Java как простой блок памяти, а массив объектов LocalDate будет представлен в виде последовательности указателей, каждый из которых ссылается на место в памяти, где был выделен объект. Хотя данные в массиве объектов LocalDate не намного сложнее, чем массив int (значение «год-месяц-день» фактически представляет собой 48 битов примитивных данных), объём используемой памяти значительно больше из-за указателей и выделенных объектов.

Что такое Value-объект

Value-объекты — это иммутабельные доменные значения. Они представляют собой объекты классов, объявленных с модификатором Value. Этот модификатор сообщает JVM: «Мне не важен адрес этого объекта в памяти, мне важны только его данные». Классы, объявленные без этого модификатора, обладают идентичностью и называются Identity-классами, а их объекты — Identity‑объектами. 

В случае с Identity‑объектами ссылка на объект хранится в переменной и позволяет найти поля объекта. Однако для этого всегда требуется дополнительный шаг — разыменование ссылки. Ссылка также кодирует уникальную идентичность объекта: каждое выполнение функции new выделяет новый объект и возвращает уникальную ссылку. Такие объекты всегда хранятся в куче, и оператор == сравнивает объекты по ссылкам. Поэтому даже если все поля двух объектов совпадают, то результат будет False.

Ссылка на Value-объект также хранится в переменной и позволяет получить доступ к его полям. Но она не служит уникальным идентификатором объекта. Для класса-значения выполнение new может не создавать новый объект, а вместо этого вернуть ссылку на существующий объект или даже ссылку, которая непосредственно воплощает объект. Оператор == сравнивает значения полей, поэтому ссылки на два объекта считаются равными, если эти объекты имеют одинаковые значения полей.

Теперь при использовании Value-объектов у JVM появляется несколько возможностей создания ссылок на Value-объекты в среде исполнения, что позволит оптимизировать использование памяти, локальность и эффективность сборки мусора. Например, ранее мы видели следующий массив, реализованный с использованием указателей на объекты в куче:

jshell> LocalDate[] dates = { d1, d1, d2, null, d3 }
dates ==> LocalDate[5] { 1996-01-23, 1996-01-23, 2026-01-23,
                         null, 1996-01-23 }

Теперь же, когда объекты LocalDate лишены идентификаторов, JVM может реализовать массив, используя ссылки, в которые напрямую кодируются поля объектов LocalDate. Каждый элемент массива может быть представлен 64-битным словом, указывающим, является ли ссылка нулевой, и если нет, то напрямую хранит значения полей года, месяца и дня Value-объекта. 

Производительность работы с таким массивом Value-объектов LocalDate может быть на уровне операций с массивом int.

Какие оптимизации предоставит Project Valhalla

Flattening

Flattening (сплющивание или выравнивание) — это механизм оптимизации, при котором JVM хранит данные объекта напрямую в месте их использования (например, в поле другого объекта или в ячейке массива), не создавая отдельный объект в куче и не используя указатель на него:

  • До Valhalla (или для обычных Identity-объектов) данные всегда хранились через косвенную адресацию (Indirection): обычный массив объектов содержал ссылки, каждая из которых вела к объекту в куче. Каждый такой объект имел заголовок (12—16 байтов) и сами данные

  • При использовании Flattening (Project Valhalla) данные Value-объектов записываются «плоско»: например, массив Point[] превратится в последовательность координат x, y, x, y,... без заголовков и указателей

К преимуществам можно отнести:

  • Снижение потребляемой памяти, так как теперь не требуется хранить множество ссылок на объекты

  • Ускорение считывания данных: когда данные лежат «плотно», за одно чтение в кеш попадает сразу много полезных значений, что исключает дорогостоящие промахи кеша

  • Снижение нагрузки на сборщик мусора: поскольку «плоские» объекты не являются отдельными сущностями в куче, сборщику мусора (Garbage Collector) нужно отслеживать меньше объектов

Scalar Replacement

Scalar Replacement (скалярная замена) — это мощная оптимизация JIT-компилятора, которая «разбирает» объект на части и заменяет его поля отдельными локальными переменными.

Скалярная замена возможна только благодаря анализу области видимости:

  • JIT-компилятор проверяет, «убегает» ли объект из метода (то есть передаётся ли в другие методы, сохраняется ли в поле другого объекта)

  • Если объект не убегает, то JIT может не создавать его в куче, а просто выделить переменные для его полей прямо в стеке или регистрах процессора

Однако существует проблема: как только вы передаёте объект в другой метод, который не был встроен (Inlined), объект «убегает». Оптимизация ломается, и JVM вынуждена аллоцировать объект в куче.

С появлением Value-объектов скалярная замена становится гораздо более агрессивной и стабильной. Поскольку Value-объекты не имеют идентичности (их адрес в памяти не важен), JVM может «разбирать» и «собирать» их на лету без риска нарушить логику программы. Это позволяет проводить скалярную замену даже там, где раньше это было невозможно. Ранее при вызове метода требовалось передавать указатель на объект. В Valhalla JIT-компилятор может передавать Value-объекты по значениям их полей через регистры процессора.

Польза от Scalar Replacement:

  • Значительное ускорение, поскольку процессор работает с данными непосредственно в регистрах

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

Расчёт потребления памяти

Самой очевидной оптимизацией при переходе на Value-объекты является снижение потребления памяти. Рассчитаем эффект оптимизации на примере классической задачи: хранение массива из 1 млн точек в двумерном пространстве.

Классические identity-объекты

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

public record Point(int x, int y) {}

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

  • Массив ссылок: 1 млн ссылок — по 4 байта при -XX:+UseCompressedOops (по умолчанию используем сжатые ссылки)

  • Объекты Point: 1 млн отдельных объектов в куче

Расчёт для одного объекта Point

Заголовок объекта

Поля (int x, int y)

Выравнивание

Итого на один объект

8—16 байтов в зависимости от JVM

8 байтов

Сумма 20 байтов не кратна 8, поэтому добавляется 4 байта padding

24 байта

Общий итог

Массив ссылок

Объекты Point

Всего

1 000 000 * 4 Б ~ 4 МБ

1 000 000 * 24 Б ~ 24 МБ

28 МБ

Value-объекты

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

Массив для Value-объектов будет представлять собой единый блок памяти, где данные x и y идут друг за другом, как в массиве примитивов.

Расчёт для одного объекта Point

Заголовок объекта

Поля (int x, int y)

Выравнивание

Итого на один объект

0 байтов

8 байтов

Не требуется

8 байтов

Общий итог

Объекты Point

Всего

1 000 000 * 8 Б ~ 8 МБ

8 МБ

Value-объекты с полем ссылочного типа

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

Предположим, мы добавили в наш value class Point ссылку на объект Label. При хранении в массиве JVM по-прежнему может применять Inlining для примитивных полей, но для Label она будет вынуждена хранить 4-байтовую ссылку.

Расчёт для одного объекта Point

Заголовок объекта

Поля (int x, int y)

Поле Label (ссылка)

Выравнивание

Итого на один объект

0 байтов

8 байтов

4 байта

Сумма 12 байтов не кратна 8, поэтому добавляется 4 байта padding

16 байтов

Общий итог

Объекты Point

Всего

1 000 000 * 16 Б ~ 16 МБ

16 МБ

Вывод

Потребление памяти сократилось почти в 3,5 раза, а доступ к данным ускорился, так как не требуется разыменование каждой ссылки на объект. Более того, ускорилась сборка мусора, так как сборщик имеет дело не с миллионом отдельных объектов для обхода, а с одним массивом. Добавление поля cо ссылкой на Identity-объект лишает Value-объект некоторых преимуществ: процессор быстро прочитает x и y, но для получения Label ему придётся снова обращаться к произвольным областям памяти. Хотя сам Point перестал быть объектом для сборщика, ссылка внутри массива заставляет его проверять миллион связей, чтобы понять, живы ли объекты Label. Если же Label тоже является Value-объектом, происходит рекурсивный инлайнинг, и весь массив превращается в плоскую структуру.

Заключение

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

На данный момент Project Valhalla доступен в сборках Early Access (доступны по ссылке), а в качестве стабильной или расширенной Preview-фичи может появиться в ближайших релизах (прогнозы указывают на Java 26-28).