JEP 450: Compact Object Headers (Experimental)
Compact Object Headers (статус Experimental (экспериментальная функция))
| Ответственный | Roman Kennke |
| Тип | Feature |
| Область | Implementation |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | hotspot / runtime |
| Обсуждение | hotspot dash dev at openjdk dot org |
| Трудоёмкость | L |
| Длительность | L |
| Связан с | JEP 519: Compact Object Headers |
| Рецензенты | Aleksey Shipilev, Erik Österlund, John Rose, Stefan Karlsson, Thomas Stuefe |
| Одобрен | Vladimir Kozlov |
| Создан | 2022/10/07 19:27 |
| Обновлён | 2025/04/15 14:03 |
| Задача | 8294992 |
Аннотация
Уменьшить размер заголовков объектов в HotSpot JVM на 64-битных архитектурах с величины от 96 до 128 бит до 64 бит. Это уменьшит размер кучи, повысит плотность развёртывания и улучшит локальность данных.
Цели
Когда эта функция включена, она
- обязана уменьшать размер заголовка объекта до 64 бит (8 байт) на целевых 64-битных платформах (x64 и AArch64),
- должна уменьшать размеры объектов и объём занимаемой памяти на реалистичных нагрузках,
- не должна увеличивать накладные расходы по пропускной способности или задержке более чем на 5% на целевых 64-битных платформах, причём допустимо это лишь в редких случаях, и
- не должна вносить измеримых накладных расходов по пропускной способности или задержке на нецелевых 64-битных платформах.
Когда эта функция выключена, она
- обязана сохранять исходную структуру заголовка объекта и исходные размеры объектов на всех платформах и
- не должна вносить измеримых накладных расходов по пропускной способности или задержке ни на одной платформе.
Эта функция в статусе Experimental сильно повлияет на реальные приложения. В коде могут быть неэффективные места, ошибки и непредвиденное поведение, не являющееся ошибками. Поэтому функция обязана быть выключена по умолчанию и включаться только по явному запросу пользователя. Мы намерены включить её по умолчанию в последующих выпусках и в конечном счёте полностью удалить код унаследованных заголовков объектов.
Что не является целью
Целью не является:
- уменьшить размер заголовка объекта ниже 64 бит на 64-битных платформах,
- уменьшить размер заголовка объекта на нецелевых 64-битных платформах,
- изменить размер заголовка объекта на 32-битных платформах, поскольку там он уже составляет 64 бита, или
- изменить кодирование содержимого объекта (т. е. полей и элементов массивов) или метаданных массивов (т. е. длины массива).
Мотивация
У объекта, хранящегося в куче, есть метаданные, которые HotSpot JVM хранит в заголовке объекта. Размер заголовка постоянен: он не зависит от типа объекта, формы массива и содержимого. В 64-битной HotSpot JVM заголовки объектов занимают от 96 бит (12 байт) до 128 бит (16 байт) в зависимости от конфигурации JVM.
Объекты в программах на Java обычно невелики. Эксперименты, проведённые в рамках проекта Lilliput, показывают, что во многих нагрузках средний размер объекта составляет от 256 до 512 бит (от 32 до 64 байт). Это означает, что более 20% живых данных может приходиться только на заголовки объектов. Поэтому даже небольшое уменьшение размера заголовка объекта может дать значительный выигрыш по объёму занимаемой памяти и локальности данных, а также снизить нагрузку на GC. Первые пользователи проекта Lilliput, опробовавшие его на реальных приложениях, подтверждают, что объём живых данных обычно сокращается на 10%–20%.
Описание
Compact Object Headers — функция в статусе Experimental, поэтому по умолчанию она выключена. Режим Compact Object Headers можно включить с помощью -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders.
Текущие заголовки объектов
В HotSpot JVM заголовки объектов обеспечивают работу многих различных механизмов:
- Сборка мусора — хранение указателей перенаправления (forwarding pointers) и отслеживание возраста объектов;
- Система типов — определение класса объекта, которое используется для вызова методов, рефлексии, проверок типов и т. д.;
- Блокировки — хранение информации о связанных с объектом лёгких и тяжёлых блокировках;
- Хеш-коды — хранение стабильного хеш-кода идентичности объекта после того, как он вычислен.
Текущая структура заголовка объекта делится на mark word и class word. Mark word идёт первым, имеет размер машинного адреса и содержит:
Mark Word (normal):
64 39 8 3 0
[.......................HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT]
(Unused) (Hash Code) (GC Age)(Tag)
В некоторых ситуациях mark word перезаписывается помеченным указателем (tagged pointer) на отдельную структуру данных:
Mark Word (overwritten):
64 2 0
[ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppTT]
(Native Pointer) (Tag)
В этом случае биты тега описывают тип указателя, хранящегося в заголовке. При необходимости исходный mark word сохраняется (вытесняется) в структуре данных, на которую указывает этот указатель, а к полям исходного заголовка, т. е. к хеш-коду и битам возраста, обращаются через разыменование указателя, чтобы добраться до вытесненного заголовка.
Class word следует за mark word. Он имеет одну из двух форм в зависимости от того, включены ли сжатые указатели на классы:
Class Word (uncompressed):
64 0
[cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc]
(Class Pointer)
Class Word (compressed):
32 0
[CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC]
(Compressed Class Pointer)
Class word никогда не перезаписывается, то есть информация о типе объекта всегда доступна, и для проверки типа или вызова метода не нужны дополнительные шаги. Важнее всего то, что частям среды выполнения, которым нужна эта информация о типе, не приходится согласовывать свою работу с подсистемами блокировок, хеширования и GC, которые могут изменять mark word.
Compact Object Headers
В режиме Compact Object Headers мы убираем разделение на mark word и class word, включая указатель на класс в сжатом виде в mark word:
Header (compact):
64 42 11 7 3 0
[CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT]
(Compressed Class Pointer) (Hash Code) /(GC Age)^(Tag)
(Valhalla-reserved bits) (Self Forwarded Tag)
Операции блокировки больше не перезаписывают mark word помеченным указателем, поэтому сжатый указатель на класс сохраняется. Операции перенаправления при GC усложняются, чтобы сохранить прямой доступ к сжатому указателю на класс; для этого нужен новый бит тега, как описано ниже. Размер хеш-кода не меняется. Мы резервируем четыре бита для будущего использования проектом Valhalla.
Сжатые указатели на классы
Нынешние сжатые указатели на классы кодируют 64-битный указатель в 32 бита. Они включены по умолчанию, но их можно выключить с помощью -XX:-UseCompressedClassPointers. Однако выключать их имело бы смысл только для приложения, которое загружает более чем примерно четыре миллиона классов; такого приложения мы пока не встречали.
Режим Compact Object Headers требует, чтобы сжатые указатели на классы были включены, и, кроме того, уменьшает размер сжатых указателей на классы с 32 до 22 бит за счёт изменения их кодирования.
Блокировки
Подсистема блокировки объектов в HotSpot JVM имеет два уровня.
-
Лёгкая блокировка (lightweight locking) используется, когда за монитор заблокированного объекта нет конкуренции, методы управления потоками (
wait(),notify()и т. д.) не вызываются и блокировки через JNI не используются. В таких случаях HotSpot атомарно меняет биты тега в заголовке объекта с01(не заблокирован) на00(заблокирован лёгкой блокировкой). Дополнительные структуры данных не нужны, другие биты заголовка не используются. -
Блокировка через монитор (monitor locking) используется, когда за монитор заблокированного объекта есть конкуренция, используются методы управления потоками или лёгкая блокировка по иным причинам не подходит. Чтобы обозначить это состояние, HotSpot атомарно меняет биты тега в заголовке объекта с
01(не заблокирован) или00(заблокирован лёгкой блокировкой) на10(заблокирован через монитор). Блокировка через монитор создаёт новую структуру данных, представляющую монитор объекта, но, как и лёгкая блокировка, не использует другие биты заголовка.
HotSpot также поддерживает унаследованный механизм блокировки на стеке (stack-locking). Этот идейный предшественник лёгкой блокировки связывает заблокированный объект с захватившим блокировку потоком: заголовок объекта копируется в стек потока, а сам заголовок объекта перезаписывается указателем на эту копию. Для Compact Object Headers это проблема, потому что заголовок объекта перезаписывается и важная информация о типе теряется. Поэтому режим Compact Object Headers несовместим с унаследованной блокировкой. Если JVM настроена на работу одновременно с унаследованной блокировкой и Compact Object Headers, то режим Compact Object Headers выключается.
Перенаправление при GC
Сборщики мусора, перемещающие объекты, делают это в два шага: сначала они копируют объект и записывают соответствие между его старой и новой копиями (т. е. перенаправление, forwarding), а затем с помощью этого соответствия обновляют ссылки на старую копию либо во всей куче, либо только в определённом поколении.
Из нынешних сборщиков мусора HotSpot только ZGC использует для записи перенаправлений отдельную таблицу перенаправления. Все остальные сборщики записывают информацию о перенаправлении, перезаписывая заголовок старой копии адресом новой копии. Есть два разных сценария, в которых участвуют заголовки.
-
Фазы копирования копируют объекты в пустое пространство. Указатель перенаправления на каждую новую копию хранится в заголовке старой копии. Исходный заголовок объекта сохраняется в новой копии. Код, который читает заголовок объекта из старой копии, переходит по указателю перенаправления к новой копии.
Если скопировать объект на новое место не удаётся, сборщики мусора устанавливают указатель перенаправления на сам объект, делая его перенаправленным на себя (self-forwarded). В режиме Compact Object Headers это перезаписало бы информацию о типе. Чтобы решить эту проблему, мы обозначаем, что объект перенаправлен на себя, установкой третьего бита заголовка объекта, а не перезаписью всего заголовка.
-
Фазы сдвига перемещают объекты, сдвигая их к младшим адресам в пределах того же пространства. Обычно это делается, когда память кучи исчерпана и места для копирования объектов не осталось. В этом случае в качестве последнего средства выполняется полная сборка сдвигающим сборщиком, которая проходит в четыре фазы:
-
Маркировка — определение множества живых объектов.
-
Вычисление адресов — обход всех живых объектов и вычисление их новых мест, т. е. мест, где они будут расположены один за другим. Эти места записываются как перенаправления в заголовки объектов.
-
Обновление ссылок — обход всех живых объектов и обновление всех ссылок на объекты так, чтобы они указывали на новые места.
-
Копирование — собственно копирование всех живых объектов на новые места.
Шаг 2 уничтожает исходные заголовки. Это проблема и для текущей реализации: если заголовок интересный, то есть в нём установлен хеш-код идентичности, информация о блокировке и т. д., то его нужно сохранить. Текущие сборщики мусора делают это, сохраняя такие заголовки в отдельной таблице и восстанавливая их после GC. Это хорошо работает, потому что объектов с интересными заголовками обычно немного. В режиме Compact Object Headers интересный заголовок есть у каждого объекта, потому что теперь заголовок содержит важную информацию о классе. Хранение большого числа сохранённых заголовков потребовало бы значительного объёма нативной кучи.
Чтобы решить эту проблему, мы используем простое кодирование указателя перенаправления, которое позволяет адресовать до 8 ТБ кучи в младших 42 битах заголовка объекта. Сейчас режим Compact Object Headers несовместим с кучами большего размера, если используются сборщики, отличные от ZGC. Если JVM настроена на использование кучи больше 8 ТБ и не использует ZGC, то режим Compact Object Headers выключается.
-
Обход кучи сборщиком мусора
Сборщики мусора часто обходят кучу, линейно просматривая объекты. Для этого нужно определять размер каждого объекта, а для этого нужен доступ к указателю на класс каждого объекта.
Когда указатель на класс закодирован в заголовке, для его декодирования нужны простые арифметические операции. Их стоимость невелика по сравнению со стоимостью обращений к памяти при обходе кучи сборщиком мусора. Дополнительной работы по реализации здесь не требуется, поскольку сборщики мусора уже обращаются к указателям на классы через общий интерфейс VM.
Альтернативы
-
Продолжать поддерживать 32-битные платформы — mark word и class word в заголовках объектов имеют размер машинного указателя, поэтому на 32-битных платформах заголовки уже занимают 64 бита. Однако сложность поддержки 32-битных портов в сочетании с уходом отрасли от 32-битных сред делает эту альтернативу непрактичной в долгосрочной перспективе.
-
Реализовать 32-битные заголовки объектов — приложив больше усилий, мы могли бы реализовать 32-битные заголовки. Скорее всего, для этого потребовалось бы реализовать отдельное хранилище хеш-кодов идентичности, выделяемое по требованию. Это наша конечная цель, но первые исследования показывают, что она потребует гораздо больше работы. Это предложение фиксирует важный промежуточный этап, который даёт существенные улучшения, и мы можем реализовать его с низким риском, продолжая двигаться к 32-битным заголовкам.
Тестирование
Изменение структуры заголовка объектов в куче Java затрагивает многие подсистемы HotSpot JVM: среду выполнения, все сборщики мусора, все JIT-компиляторы, интерпретаторы, serviceability agent и платформенно-зависимый код для всех поддерживаемых платформ. Столь масштабные изменения требуют столь же масштабного тестирования.
Compact Object Headers будут протестированы с помощью:
- тестов уровней Tier 1–4 и, возможно, тестов дополнительных уровней у тех поставщиков, у которых они есть;
- наборов бенчмарков SPECjvm, SPECjbb, DaCapo и Renaissance для проверки как корректности, так и производительности;
- JCStress для проверки новой реализации блокировок; и
- ряда реальных рабочих нагрузок.
Все эти тесты будут выполняться с включённой и выключенной функцией, с различными сочетаниями сборщиков мусора и JIT-компиляторов, а также на нескольких аппаратных платформах.
Мы также добавим новый набор тестов, измеряющих размер различных объектов, например обычных объектов, массивов примитивных типов, массивов ссылок, и их заголовков.
Окончательной проверкой производительности и корректности станут реальные рабочие нагрузки после выпуска этой функции в статусе Experimental.
Риски и допущения
-
Будущим возможностям среды выполнения нужны биты заголовка объекта — это предложение не оставляет в заголовке свободных битов для будущих возможностей, которым такие биты могут понадобиться. Организационно мы снижаем этот риск, обсуждая потребности в заголовке объекта с другими крупными проектами JDK, например проектом Valhalla. Технически мы снижаем этот риск, исходя из того, что хеш-коды идентичности и сжатые указатели на классы можно сократить ещё больше, чтобы освободить биты, если они понадобятся будущим возможностям среды выполнения.
-
Ошибки реализации в коде функции — обычный риск для столь глубоко затрагивающей систему функции — ошибки в реализации. Проблемы в структуре заголовка, скорее всего, сразу проявятся в большинстве тестов, однако тонкости новых протоколов блокировки и перенаправления (forwarding) при сборке мусора могут выявлять ошибки лишь изредка. Мы снижаем этот риск тщательным рецензированием кода ответственными за компоненты и запуском множества тестов с включённой функцией. Этот риск не затрагивает продукт, пока функция остаётся в статусе Experimental и выключена по умолчанию.
-
Ошибки реализации в унаследованном коде — мы стараемся не менять унаследованные пути выполнения кода, но часть рефакторингов неизбежно затрагивает общий код. Из-за этого возникает риск ошибок, даже когда функция отключена. Помимо тщательного рецензирования и тестирования, мы снижаем этот риск, программируя защитно и стараясь не изменять общие пути выполнения кода, даже если это требует больше работы в путях кода самой функции.
-
Проблемы производительности в коде функции — более сложные протоколы Compact Object Headers могут вызвать проблемы производительности при включённой функции. Мы снижаем этот риск, запуская основные бенчмарки и разбираясь во влиянии функции на их производительность. Потери производительности связаны с косвенным доступом к указателю на класс, использованием альтернативной схемы блокировки на стеке и применением альтернативного механизма скользящего перенаправления (sliding forwarding) при сборке мусора. Этот риск не затрагивает продукт, пока функция остаётся в статусе Experimental и выключена по умолчанию.
-
Проблемы производительности в унаследованном коде — существует небольшой риск того, что рефакторинг унаследованных путей выполнения кода неожиданным образом повлияет на производительность. Мы снижаем этот риск, сводя к минимуму изменения в унаследованных путях выполнения кода и показывая, что производительность основных рабочих нагрузок существенно не меняется.
-
Поддержка сжатых указателей на классы — JVMCI на x64 не поддерживает сжатые указатели на классы. Непосредственный риск мы снижаем, отключая Compact Object Headers при включённом JVMCI. Долгосрочный риск состоит в том, что компактные заголовки так и не будут реализованы в JVMCI, и это навсегда заблокирует удаление унаследованной реализации заголовков. Мы оцениваем вероятность этого риска как незначительную, поскольку другие JIT-компиляторы поддерживают Compact Object Headers без глубоких изменений.
-
Кодирование сжатых указателей на классы — как сказано выше, текущая реализация сжатых указателей на классы ограничена примерно четырьмя миллионами классов. Сейчас пользователи могут обойти это ограничение, отключив сжатые указатели на классы, но если мы удалим унаследованную реализацию заголовков, такой возможности больше не будет. Непосредственный риск мы снижаем, предоставляя Compact Object Headers как функцию в статусе Experimental; в долгосрочной перспективе мы намерены работать над более эффективными схемами кодирования сжатых указателей на классы.
-
Изменение низкоуровневых интерфейсов — некоторым компонентам, которые работают с заголовками объектов напрямую, в особенности компилятору Graal как основному пользователю JVMCI, придётся реализовать новую структуру заголовка. Текущий риск мы снижаем, выявляя такие компоненты и отключая функцию, когда эти компоненты используются. Прежде чем функция выйдет из статуса Experimental, эти компоненты нужно будет доработать.
-
Мягкий провал проекта — существует небольшой риск того, что у функции окажутся неустранимые функциональные регрессии по сравнению с унаследованной реализацией, например ограничение количества представимых классов. Связанный с этим риск состоит в том, что функция, сама по себе давая значительный прирост производительности, будет иметь существенные функциональные ограничения, и это может стать доводом в пользу того, чтобы навсегда сохранить и новую, и унаследованную реализацию заголовков. Поскольку цель этой работы — в конечном итоге заменить унаследованную реализацию заголовков, мы считаем такой исход мягким провалом проекта. Мы снижаем этот риск, тщательно изучая текущие ограничения, планируя дальнейшую работу по их устранению и опираясь на отчёты первых пользователей, чтобы выявить другие риски до того, как вложим слишком много усилий.
-
Жёсткий провал проекта — хотя это очень маловероятно, может оказаться, что Compact Object Headers не дают ощутимых улучшений в реальных условиях или что достижимые улучшения не оправдывают дополнительной сложности. Мы снижаем этот незначительный риск, помечая новые пути выполнения кода как Experimental и тем самым оставляя возможность при необходимости удалить функцию в одном из будущих выпусков.