Перейти к основному содержимому

Сжатые ООП в JVM

· 6 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3. Сжатые ООП

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

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

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

./fc57aa90080036066c7a20977f5c998e.jpg

Поскольку JVM уже знает, что последние три бита всегда равны нулю, нет смысла хранить эти незначащие нули в куче. Вместо этого он предполагает, что они есть, и сохраняет 3 других более значимых бита, которые мы не могли вместить в 32-битные ранее. Теперь у нас есть 32-битный адрес с 3 сдвинутыми вправо нулями, поэтому мы сжимаем 35-битный указатель в 32-битный. Это означает, что мы можем использовать до 32 ГБ — 2 ^32+3 = 2 ^35 = 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. Футуристические сборщики мусора

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 .