«1. Обзор

JVM управляет памятью за нас. Это снимает с разработчиков бремя управления памятью, поэтому нам не нужно вручную манипулировать указателями объектов, что, как доказано, требует много времени и чревато ошибками.

Под капотом JVM реализовано множество изящных трюков для оптимизации процесса управления памятью. Одним из приемов является использование сжатых указателей, которые мы собираемся оценить в этой статье. Во-первых, давайте посмотрим, как JVM представляет объекты во время выполнения.

2. Представление объектов во время выполнения

JVM HotSpot использует для представления объектов структуру данных, называемую oops или обычные указатели объектов. Эти oop эквивалентны родным указателям C. instanceOops — это особый вид oop, который представляет экземпляры объектов в Java. Кроме того, JVM также поддерживает несколько других операций, которые хранятся в исходном дереве OpenJDK.

Давайте посмотрим, как JVM размещает instanceOops в памяти.

2.1. Структура памяти объекта

Структура памяти instanceOop проста: это просто заголовок объекта, за которым сразу следует ноль или более ссылок на поля экземпляра.

Представление JVM заголовка объекта состоит из:

    Одно слово метки служит многим целям, таким как предвзятая блокировка, хеш-значения идентификаторов и сборщик мусора. Это не oop, но по историческим причинам он находится в исходном дереве oop OpenJDK. Кроме того, состояние слова метки содержит только uintptr_t, поэтому его размер варьируется от 4 до 8 байтов в 32-битной и 64-битной архитектурах соответственно. Одно, возможно, сжатое слово класса, которое представляет собой указатель на метаданные класса. До Java 7 они указывали на постоянное поколение, но начиная с Java 8 они указывали на 32-битный пробел Metaspace A для принудительного выравнивания объектов. Это делает компоновку более удобной для аппаратного обеспечения, как мы увидим позже

Сразу после заголовка должно быть ноль или более ссылок на поля экземпляра. В этом случае слово является собственным машинным словом, то есть 32-разрядным на устаревших 32-разрядных машинах и 64-разрядным на более современных системах.

Заголовок объекта массивов, помимо слов mark и klass, содержит 32-битное слово для представления его длины.

2.2. Анатомия отходов

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

Основным виновником возможного снижения производительности являются 64-битные ссылки на объекты. 64-битные ссылки занимают вдвое больше места, чем 32-битные ссылки, поэтому это приводит к большему потреблению памяти в целом и более частым циклам сборки мусора. Чем больше времени посвящено циклам GC, тем меньше фрагментов выполнения ЦП для потоков нашего приложения.

Итак, должны ли мы снова вернуться к 32-битной архитектуре? Даже если бы это был вариант, мы не могли бы иметь более 4 ГБ пространства кучи в 32-разрядных пространствах процессов без дополнительной работы.

3. Сжатые ООП

Как оказалось, JVM может избежать потери памяти за счет сжатия указателей объектов или ООП, поэтому мы можем получить лучшее из обоих миров: разрешение более 4 ГБ пространства кучи с 32- ссылки на биты в 64-битных машинах!

3.1. Базовая оптимизация

Как мы видели ранее, JVM добавляет отступы к объектам, чтобы их размер был кратен 8 байтам. С этими дополнениями последние три бита в oops всегда равны нулю. Это связано с тем, что числа, кратные 8, всегда заканчиваются на 000 в двоичном формате.

Поскольку JVM уже знает, что последние три бита всегда равны нулю, нет смысла хранить эти незначащие нули в куче. Вместо этого он предполагает, что они есть, и сохраняет 3 других более значимых бита, которые мы не могли вместить в 32-битные ранее. Теперь у нас есть 32-битный адрес с 3 сдвинутыми вправо нулями, поэтому мы сжимаем 35-битный указатель в 32-битный. Это означает, что мы можем использовать до 32 ГБ (232+3=235=32 ГБ) пространства кучи без использования 64-битных ссылок.

«Чтобы эта оптимизация работала, когда JVM нужно найти объект в памяти, она сдвигает указатель влево на 3 бита (по сути, добавляет эти 3 нуля обратно в конец). С другой стороны, при загрузке указателя в кучу JVM сдвигает указатель вправо на 3 бита, чтобы отбросить ранее добавленные нули. По сути, JVM выполняет немного больше вычислений, чтобы сэкономить место. К счастью, смещение битов — тривиальная операция для большинства процессоров.

Чтобы включить сжатие oop, мы можем использовать флаг настройки -XX:+UseCompressedOops. Сжатие oop — это поведение по умолчанию, начиная с Java 7, когда максимальный размер кучи меньше 32 ГБ. Когда максимальный размер кучи превышает 32 ГБ, JVM автоматически отключит сжатие oop. Таким образом, использование памяти за пределами размера кучи 32 Гб должно управляться по-другому.

3.2. Более 32 ГБ

Также можно использовать сжатые указатели, когда размер кучи Java превышает 32 ГБ. Хотя выравнивание объекта по умолчанию составляет 8 байтов, это значение можно настроить с помощью флага настройки -XX:ObjectAlignmentInBytes. Указанное значение должно быть степенью двойки и должно находиться в диапазоне от 8 до 256.

Мы можем рассчитать максимально возможный размер кучи со сжатыми указателями следующим образом:

4 GB * ObjectAlignmentInBytes

Например, при выравнивании объекта составляет 16 байт, мы можем использовать до 64 ГБ пространства кучи со сжатыми указателями.

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

3.3. Футуристические GC

ZGC, новое дополнение в Java 11, был экспериментальным и масштабируемым сборщиком мусора с малой задержкой.

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

Начиная с Java 15, ZGC поддерживает сжатые указатели классов, но по-прежнему не поддерживает сжатые ООП.

Однако все новые алгоритмы GC не будут жертвовать памятью ради малой задержки. Например, сборщик мусора Shenandoah поддерживает сжатые ссылки в дополнение к тому, что он является сборщиком мусора с малым временем паузы.

Более того, и Shenandoah, и ZGC доработаны для Java 15.

4. Заключение

В этой статье мы описали проблему управления памятью JVM в 64-битных архитектурах. Мы рассмотрели сжатые указатели и выравнивание объектов и увидели, как JVM может решить эти проблемы, позволяя нам использовать большие размеры кучи с менее расточительными указателями и минимумом дополнительных вычислений.

Для более подробного обсуждения сжатых ссылок настоятельно рекомендуется ознакомиться с еще одним замечательным произведением Алексея Шипилова. Кроме того, чтобы увидеть, как распределение объектов работает внутри HotSpot JVM, ознакомьтесь со статьей Memory Layout of Objects in Java.