«1. Обзор

В этой быстрой статье мы рассмотрим, каково влияние логического значения в JVM при различных обстоятельствах.

Сначала мы проверим JVM, чтобы увидеть размеры объектов. Тогда мы поймем обоснование этих размеров.

2. Настройка

Чтобы проверить расположение объектов в памяти JVM, мы будем широко использовать Java Object Layout (JOL). Поэтому нам нужно добавить зависимость jol-core:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

3. Размеры объектов

Если мы попросим JOL распечатать сведения о виртуальной машине с точки зрения размеров объектов:

System.out.println(VM.current().details());

Когда сжатые ссылки включены (поведение по умолчанию), мы увидим вывод:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

В первых нескольких строках мы можем увидеть некоторую общую информацию о виртуальной машине. После этого мы узнаем о размерах объектов:

    Ссылки Java занимают 4 байта, логические значения/байты — 1 байт, символы/шорты — 2 байта, целые/плавающие числа — 4 байта и, наконец, длинные/двойные числа — 8 байтов. Эти типы потребляют один и тот же объем памяти, даже когда мы используем их как элементы массива

Итак, при наличии сжатых ссылок каждое логическое значение занимает 1 байт. Точно так же каждое логическое значение в boolean[] занимает 1 байт. Однако отступы выравнивания и заголовки объектов могут увеличить пространство, занимаемое логическими значениями и логическими значениями [], как мы увидим позже.

3.1. Нет сжатых ссылок

Даже если мы отключим сжатые ссылки с помощью -XX:-UseCompressedOops, логический размер вообще не изменится:

# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

С другой стороны, ссылки Java занимают вдвое больше памяти.

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

3.2. Разрыв слов

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

Одной из целей разработки JVM является предотвращение этого явления, известного как разрыв слов. То есть в JVM каждое поле и элемент массива должны быть разными; обновления одного поля или элемента не должны взаимодействовать с чтением или обновлением любого другого поля или элемента.

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

4. Обычные указатели объектов (ООП)

Теперь, когда мы знаем, что логические значения имеют размер 1 байт, давайте рассмотрим этот простой класс:

class BooleanWrapper {
    private boolean value;
}

Если мы проверим структуру памяти этого класса с помощью JOL:

System.out.println(ClassLayout.parseClass(BooleanWrapper.class).toPrintable());

Затем JOL напечатает макет памяти:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     1   boolean BooleanWrapper.value                      N/A
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

Макет BooleanWrapper состоит из:

    12 байтов для заголовка, включая два слова метки и одно слово класса. HotSpot JVM использует слово метки для хранения метаданных сборщика мусора, идентификационного хэш-кода и информации о блокировке. Кроме того, он использует слово klass для хранения метаданных класса, таких как проверки типа во время выполнения 1 байт для фактического логического значения 3 байта заполнения для целей выравнивания

По умолчанию ссылки на объекты должны быть выровнены по 8 байтам. Поэтому JVM добавляет 3 байта к 13 байтам заголовка и логического значения, чтобы получить 16 байтов.

Таким образом, логические поля могут потреблять больше памяти из-за их выравнивания полей.

4.1. Пользовательское выравнивание

Если мы изменим значение выравнивания на 32 с помощью -XX:ObjectAlignmentInBytes=32, то тот же макет класса изменится на:

OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     1   boolean BooleanWrapper.value                      N/A
     13    19           (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 19 bytes external = 19 bytes total

Как показано выше, JVM добавляет 19 байтов заполнения, чтобы сделать объект размер кратен 32.

5. ООП массива

Давайте посмотрим, как JVM размещает логический массив в памяти:

boolean[] value = new boolean[3];
System.out.println(ClassLayout.parseInstance(value).toPrintable());

Это напечатает макет экземпляра следующим образом:

OFFSET  SIZE      TYPE DESCRIPTION                              
      0     4           (object header)  # mark word
      4     4           (object header)  # mark word
      8     4           (object header)  # klass word
     12     4           (object header)  # array length
     16     3   boolean [Z.<elements>    # [Z means boolean array                        
     19     5           (loss due to the next object alignment)

Помимо двух слов-меток и одного слова-класса, указатели массива содержат дополнительные 4 байта для хранения их длины.

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

Хотя размер каждого логического элемента в массиве составляет всего 1 байт, весь массив занимает гораздо больше памяти. Другими словами, мы должны учитывать накладные расходы на заголовок и заполнение при вычислении размера массива.

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

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

Для более подробного обсуждения настоятельно рекомендуется ознакомиться с разделом oops исходного кода JVM. Также у Алексея Шипилова есть гораздо более глубокая статья в этой области.

Как обычно, все примеры доступны на GitHub.