О размерах в Java
26 Sep 2012Задался недавно вопросом: "Как правильно оценить размер выделяемой памяти под объекты в 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.
тип | размер (байт) | размер в массиве (байт) | допустимые значения |
---|---|---|---|
byte | 1 | 1 | -128 .. 127 |
short | 2 | 2 | -32768 .. 32767 |
chart | 2 | 2 | '\u0000' .. '\uffff' |
int | 4 | 4 | -2147483648 .. 2147483647 |
float | 4 | 4 | -3.4028235e+38f .. 3.4028235e+38f |
long | 8 | 8 | -9223372036854775808 .. 9223372036854775807 |
double | 8 | 8 | -1.7976931348623157e+308 .. 1.7976931348623157e+308 |
boolean | 4 | 1 | false, 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 |
---|---|---|
x86 | 8 байт (4 + 4 ) | |
x86_64 | 16 байт (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