Задался недавно вопросом: "Как правильно оценить размер выделяемой памяти под объекты в Java?". На хабре есть несколько статей [1], [2] посвященных этому вопросу. Но мне не совсем понравился подход, использованный авторами. Поэтому решил заглянуть внутрь OpenJDK Hotspot VM (далее по тексту Hotspot) и попытаться понять как все устроено на самом деле.

Типы данных в Java

  • Примитивы. (byte, short, char, int, float, long, double, boolean).
  • Объекты. Размер объекта зависит от конкретной реализации VM и архитектуры процессора. Поэтому дать однозначный ответ не получится. Все же хочется понять (на примере конкретной VM) какой размер памяти выделяется под java-объект.
  • Массивы. Одномерные линейные структуры, которые могут содержать все перечисленные типы (включая другие массивы). Массивы также являются объектами, но со специфичной структурой.

Примитивы

С размером примитивов все понятно - их размер определен в спецификации языка (JLS 4.2) и спецификации jvm (JVMS 2.3). Интересно заметить, что для типа boolean jvm использует int, а не byte как могло бы показаться (JMS 2.3.4). Также интересно, что при создании массива boolean[] под каждый элемент массива будет выделен 1 байт, а не 4.

тип размер (байт) размер в массиве (байт) допустимые значения
byte11-128 .. 127
short22-32768 .. 32767
chart22'\u0000' .. '\uffff'
int44-2147483648 .. 2147483647
float44-3.4028235e+38f .. 3.4028235e+38f
long88-9223372036854775808 .. 9223372036854775807
double88-1.7976931348623157e+308 .. 1.7976931348623157e+308
boolean41false, true

Объекты

Для описания экземпляров массивов Hotspot использует класс arrayOopDesc, для описания остальных Java-классов используется класс instanceOopDesc. Оба эти класса наследуются от oopDesc и оба содержат методы для вычисления размера заголовка. Так например a instabceOopDesc вычисляет размер заголовка (в машинных словах) следующим образом:

static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

где HeapWordSize определяется как размер указателя. В зависимости от архитектуры CPU 4 и 8 байт для x86 и x86_64 (в Oracle именуют x64) соответственно. Чтобы понять размер instanceOopDesc надо заглянуть в oopDesc, так как в самом instanceOopDesc никаких полей не объявлено. Вот что мы там увидим:

class oopDesc {
   // ...
    volatile markOop  _mark;
    union _metadata {
        wideKlassOop    _klass;
        narrowOop       _compressed_klass;
    } _metadata;
    // ...
};

В файле oopsHierarchy.hpp объявлены необходимые типы данных для работы с иерархией объектов oop (ordinary object pointer). Посмотрим как объявлены те типы, которые используются в oopDesc:

// ...
typedef juint narrowOop; // Offset instead of address for an oop within a java object
typedef class klassOopDesc* wideKlassOop; // to keep SA happy and unhandled
                                          // oop detector happy.
// ...
typedef class markOopDesc*  markOop;
// ...

То есть это два указателя (читай два машинных слова) для конкретной архитектуры - так называемое маркировочное слово (mark word) и адрес (который может быть представлен указателем или смещением) на метаданные класса. Идея этого union metadata состоит в том, что при включенной опции -XX:+UseCompressedOops будет использоваться 32х битное смещение (_compressed_klass) а не 64х битный адрес (_klass). Получается размер заголовка java-объекта 8 байт для x86 и 16 байт для x86_64 в не зависимости от параметра UseCompressedOops:

Архитектура -XX:+UseCompressedOops -XX:-UseCompressedOops
x868 байт (4 + 4)
x86_6416 байт (8 + 8)

Массивы

В arrayOopDesc размер заголовка вычисляется следующим образом:

static int header_size_in_bytes() {
    size_t hs = align_size_up(length_offset_in_bytes() + sizeof(int), HeapWordSize);
    // ...
    return (int)hs;
}

где

  • align_size_up - инлайнер для выравнивания первого аргумента по второму. Например align_size_up(12, 8) = 16.
  • length_offset_in_bytes - возвращает размер заголовка в байтах в зависимости от опции -XX:+UseCompressedOops. Если она включена, то размер равен sizeof(markOop) + sizeof(narrowOop) = 8 (4 + 4) байт для x86 и 12 (8 + 4) байт для x86_64. При выключенной опции размер равен sizeof(arrayOopDesc) = 8 байт для x86 и 16 байт для x86_64.
  • заметьте, что к вычисленному размеру прибавляется sizeof(int). Это делается для того чтобы "зарезервировать место" под поле length массива, так как оно явно не определено в классе. При включенной ссылочной компрессии (актуально только для 64x битной архитектуры) это поле займет вторую половину поля _klass (см. класс oopDesc)

Посчитаем, что у нас получается. Размер заголовка массива после выравнивания:

Архитектура -XX:+UseCompressedOops -XX:-UseCompressedOops
x86 12 байт (4 + 4 + 4 align 4)
x86_64 16 байт (8 + 4 + 4 align 8) 24 байта (8 + 8 + 4 align 8)

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

Для предотвращения ситуаций ложного совместного использования строки кэша (cache-line false sharing) размер объекта в Hotspot выравнивается по 8 байтовой границе. То есть если объект будет занимать даже 1 байт под него выделится 8 байт. Размер границы выравнивания выбирается таким образом, чтобы строка кэша была кратна этой границе, а также эта граница должна быть степенью двойки, а также кратна машинному слову. Так как у большинства современных процессоров размер строки кэша составляет 64 байта, а размер машинного слова - 4/8 байт, то размер границы был выбран равным 8 байт. В файле globalDefinitions.hpp есть соответствующие определения (строки 372 - 390). Здесь не буду приводить, интересующиеся могут сходить и посмотреть.

Начиная с версии jdk6u21 размер выравнивания стал настраиваемым параметром. Его можно задать при помощи параметра -XX:ObjectAlignmentInBytes=n. Допустимы значения 8 и 16.

И что же получается?

А получается следующая картина (для x86_64):

public class Point {                         // 0x00 +------------------+
    private int x;                           //      | mark word        |  8 bytes
    private int y;                           // 0x08 +------------------+
    private byte color;                      //      | klass oop        |  8 bytes
                                             // 0x10 +------------------+
    public Point(int x, int y, byte color) { //      | x                |  4 bytes
        this.x = x;                          //      | y                |  4 bytes
        this.y = y;                          //      | color            |  1 byte
        this.color = color;                  // 0x19 +------------------+
    }                                        //      | padding          |  7 bytes
                                             //      |                  |
    // ...                                   // 0x20 +------------------+
}                                            //                    total: 32 bytes

Для массива char[] из 11 элементов (для x86_64):

char[] str = new char[] {                    // 0x00 +------------------+
    'H', 'e', 'l', 'l', 'o', ' ',            //      | mark word        |  8 bytes
    'W', 'o', 'r', 'l', 'd' };               // 0x08 +------------------+
                                             //      | klass oop        |  4 bytes
                                             // 0x0c +------------------+
                                             //      | length           |  4 bytes
                                             // 0x10 +------------------+
                                             //      | 'H'              | 22 bytes
                                             //      | 'e'              |
                                             //      | 'l'              |
                                             //      | 'l'              |
                                             //      | 'o'              |
                                             //      | ' '              |
                                             //      | 'W'              |
                                             //      | 'o'              |
                                             //      | 'r'              |
                                             //      | 'l'              |
                                             //      | 'd'              |
                                             // 0x26 +------------------+
                                             //      | padding          |  2 bytes
                                             // 0x28 +------------------+
                                             //                    total: 40 bytes

Что почитать по теме