(пост из серии «будни перформанс-инженеров»)
Привет! Наблюдая достаточно много постов про занимаемое объектами пространство в Java, решил написать пост, срывающий покровы. Для понимания происходящего неплохо было бы ориентироваться в азах устройства Java heap'а, минимальных размерах базовых типов, продвинутых штук, типа сжатых указателей.
Часть первая. Мифы.
Миф 0. Можно раз и навсегда узнать, сколько будет занимать объект в памяти.
Реальность: Зависит как минимум от: а) целевой JVM, будь то HotSpot, JRockit, J9 или ещё что-нибудь; б) битности, как минимум размеры указателей могут различаться, а то и базовые типы могут быть представлены другими размерами (при поддержке семантики языка), в) включённых и случившихся оптимизаций, типа инлайна объектов, скаляризаций, паддингов, г) и ещё тучи всяких штук, по сравнению с которыми фазы Луны куда более предсказуемы.
Миф 1. Для подсчёта размера объекта достаточно сложить размеры полей. Всем известно, сколько занимает поле конкретного примитивного типа, а уж тем более, сколько занимает ссылка.
Реальность: про размеры базовых типов см. Миф 0. Кроме всего прочего конкретные платформы могут требовать выравнивания полей из соображений корректности (некоторые процы вообще не работают с misaligned данными), либо из соображений производительности. Далее, во многих случаях объекты тоже должны быть выровнены, и поэтому за каждым инстансом будет следовать паддинг. Кроме того, есть ещё заголовок объекта…
Миф 2. Для подсчёта размера объекта достаточно сложить размеры полей и погуглить размеры заголовков объектов в распространённых реализациях JVM.
Реальность: у этой информации достаточно маленький «shelf life», т.е. она довольно резво устаревает. Например, в JRockit'е заголовки могут быть 8 байт, а в HotSpot'е — до 16. Но это только пока мы не доберёмся переделать схему блокировок, и в HS не появятся такие же сжатые заголовки. И тогда половина интернетов будет бить себя пяткой в грудь про 16-байтовые заголовки, а вторая — про 8-байтовые.
Миф 3. Любой нормальный инструмент покажет реальный размер объекта.
Реальность: ха-ха, зависит от определения нормальности. «Нормальный» HotSpot'овский HPROF, например, будет считать размеры объекта так, как описано в мифе 1. По этому поводу все тулы, вроде jhat, MAT, и прочих post-mortem тулов обречены на пере/недооценку размера объектов.
Мораль: Нам нужны online-тулы, которые бы давали информацию о размере объектов прямо на месте.
Часть вторая. Реальности.
Возмём для примера обычный такой HashMap, на обычном таком Linux x86_64. Инстанциируем один HashMap, сделаем хипдамп и посмотрим на этот хипдамп разными инструментами. Вот такие размеры HashMap'а в байтах нам рапортуют эти инструменты:
JVM | VisualVM | Eclipse MAT |
---|---|---|
HotSpot (7u10) x86 | 45 | 48 |
HotSpot (7u10) x86_64 | 69 | 72 |
HotSpot (7u10) x86_64 (compressed refs) | 69 | 56 |
JRockit (6u37, R28.2.5) x86 | 48 | 48 |
JRockit (6u37, R28.2.5) x86_64 | 76 | 80 |
JRockit (6u37, R28.2.5) x86_64 (compressed refs) | 76 | 56 |
Как видно, тулы друг с другом во многом не соглашаются, и на то есть причина: в формате HPROF'а отсутствует информация о расположении полей, поэтому в конце концов приходится гадать о размерах заголовков, размерах полей и заниматься прочей магией.
Однако, у нас есть магия более высокого уровня, когда мы можем напрямую у VM спросить о структуре объектов, но для этого нам придётся пользоваться Unsafe (дьявольский смех), diagnostic MXBeans (дьявольский смех) и делать всё это в онлайне (дьявольский смех). Всё это счастье уже объединено в мой плюшевый проектик на GitHub: https://github.com/shipilev/java-object-layout
Он точно работает для HotSpot и JRockit; может приемлемо работать и для других VM (буду признателен, если кто-нибудь дофиксит это до J9, SAP и прочих, мне по некоторым причинам юридического толка этим заниматься нельзя).
Посмотрим на всё тот же HashMap при помощи нашего волшебного тула:
HotSpot x86:
Running 32-bit HotSpot VM.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 8 (assumed to be the object header + first field alignment)
8 4 Set AbstractMap.keySet
12 4 Collection AbstractMap.values
16 4 int HashMap.size
20 4 int HashMap.threshold
24 4 float HashMap.loadFactor
28 4 int HashMap.modCount
32 4 int HashMap.hashSeed
36 1 boolean HashMap.useAltHashing
37 3 (alignment/padding gap)
40 4 Entry[] HashMap.table
44 4 Set HashMap.entrySet
48 (object boundary, size estimate)
VM reports 48 bytes per instance
Что мы видим в этом дампе?
- Заголовок занимает 8 байт, что соответствует известным нам описаниям HS — заголовок состоит из mark word'а с флажками состояния и klass word'а, показывающего на метаинформацию класса
- Объекты выровнены на 8 байт (поспекулирую, что это ради выравнивания long'ов и double'ов)
- Ссылки занимают по 32 бита, выровнены на 4 внутри объекта, что вкупе с выравниванием объектов даёт выравнивание на 4 в памяти
- Одинокое boolean-поле индуцирует после себя паддинг, чтобы выровнять следующий указатель; если бы в классе оказалось ещё одно мелкое поле, то можно было бы ожидать, что оно займёт часть паддинга — обратите внимание, что это не увеличит размер объекта ;)
HotSpot x86_64:
Running 64-bit HotSpot VM.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 16 (assumed to be the object header + first field alignment)
16 8 Set AbstractMap.keySet
24 8 Collection AbstractMap.values
32 4 int HashMap.size
36 4 int HashMap.threshold
40 4 float HashMap.loadFactor
44 4 int HashMap.modCount
48 4 int HashMap.hashSeed
52 1 boolean HashMap.useAltHashing
53 3 (alignment/padding gap)
56 8 Entry[] HashMap.table
64 8 Set HashMap.entrySet
72 (object boundary, size estimate)
VM reports 72 bytes per instance
Поанализируем:
- Заголовок стал занимать 16 байт, потому что mark word и klass word теперь полные 64-битные слова.
- Объекты по-прежнему выровнены на 8 байт.
- Ссылки занимают по 64 бита, начали выравниваться по 8 байт.
HotSpot x86_64 (compressed references):
Running 64-bit HotSpot VM.
Using compressed references with 3-bit shift.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 12 (assumed to be the object header + first field alignment)
12 4 Set AbstractMap.keySet
16 4 Collection AbstractMap.values
20 4 int HashMap.size
24 4 int HashMap.threshold
28 4 float HashMap.loadFactor
32 4 int HashMap.modCount
36 4 int HashMap.hashSeed
40 1 boolean HashMap.useAltHashing
41 3 (alignment/padding gap)
44 4 Entry[] HashMap.table
48 4 Set HashMap.entrySet
52 4 (loss due to the next object alignment)
56 (object boundary, size estimate)
VM reports 56 bytes per instance
Поанализируем:
- Заголовок стал занимать 12 байт, потому что klass word теперь представлен сжатым указателем.
- Объекты по-прежнему выровнены на 8 байт.
- Ссылки занимают по 32 бита, выравниваются на 4 байта, и поэтому мы чуть-чуть потеряли в хвосте.
JRockit x86:
Running 32-bit JRockit VM.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 8 (assumed to be the object header + first field alignment)
8 4 Set AbstractMap.keySet
12 4 Collection AbstractMap.values
16 4 Entry[] HashMap.table
20 4 Object[] HashMap.cache
24 4 Set HashMap.entrySet
28 4 int HashMap.cache_bitmask
32 4 int HashMap.size
36 4 int HashMap.threshold
40 4 float HashMap.loadFactor
44 4 int HashMap.modCount
48 (object boundary, size estimate)
VM reports 48 bytes per instance
Поанализируем:
- Заголовок, так же как и в 32-bit HS, 8 байт, т.е. 2 слова.
- Объекты выровнены на 8 байт, но с количеством полей у нас подгадано так, что потерь на выравнивание нет.
- Ссылки занимают по 32 бита, выравниваются на 4 байта.
- И самое главное: это не тот же самый HashMap, сравните со структурой в HS! (сколько народу погорело на «знании», что JRockit — это «только» VM)
JRockit x86_64:
Running 64-bit JRockit VM.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 8 (assumed to be the object header + first field alignment)
8 8 Set AbstractMap.keySet
16 8 Collection AbstractMap.values
24 8 Entry[] HashMap.table
32 8 Object[] HashMap.cache
40 8 Set HashMap.entrySet
48 4 int HashMap.cache_bitmask
52 4 int HashMap.size
56 4 int HashMap.threshold
60 4 float HashMap.loadFactor
64 4 int HashMap.modCount
68 4 (loss due to the next object alignment)
72 (object boundary, size estimate)
VM reports 72 bytes per instance
Поанализируем:
- Заголовок, опа-опа,
гандам-стайлзанимает всего 8 байт, т.е. одно машинное слово! (Клёвая фича, которую сейчас портируют в HS, и которая требует переработки внутренного механизма синхронизации) - Объекты выровнены на 8 байт, поэтому потеряли немножко в хвосте
- Ссылки занимают по 64 бита, выравниваются на 8 байт.
JRockit x86-64, compressed refs:
Running 64-bit JRockit VM.
Using compressed references with 0-bit shift.
Objects are 8 bytes aligned.
java.util.HashMap
offset size type description
0 8 (assumed to be the object header + first field alignment)
8 4 Set AbstractMap.keySet
12 4 Collection AbstractMap.values
16 4 Entry[] HashMap.table
20 4 Object[] HashMap.cache
24 4 Set HashMap.entrySet
28 4 int HashMap.cache_bitmask
32 4 int HashMap.size
36 4 int HashMap.threshold
40 4 float HashMap.loadFactor
44 4 int HashMap.modCount
48 (object boundary, size estimate)
VM reports 48 bytes per instance
Анализ:
- Заголовок по-прежнему 8 байт.
- Ссылки сжались до 32 бит, что в т.ч. дало возможность лихо вписаться в 8-байтовое выравнивание.
- Обратите внимание, что объект занимает ровно столько же, сколько и на 32-битной платформе, всё из-за заголовка такого же размера.
Таким образом, можно посмотреть на реальные размеры объектов в полной табличке:
JVM | VisualVM | Eclipse MAT | java-object-layout |
---|---|---|---|
HotSpot (7u10) x86 | 45 (врёт) | 48 | 48 |
HotSpot (7u10) x86_64 | 69 (врёт) | 72 | 72 |
HotSpot (7u10) x86_64 (compressed refs) | 69 (врёт) | 56 | 56 |
JRockit (6u37, R28.2.5) x86 | 48 | 48 | 48 |
JRockit (6u37, R28.2.5) x86_64 | 76 (врёт) | 80 (врёт) | 72 |
JRockit (6u37, R28.2.5) x86_64 (compressed refs) | 76 (врёт) | 56 (врёт) | 48 |
Как видно, только в тривиальных случаях у нас всё хорошо, но чуть в сторону — враньё.
Часть третья. Извращения.
У волшебного HS есть волшебная опция -XX:ObjectAlignmentInBytes, которая управляет выравниванием объектов (кроме всего прочего, она позволяет использовать сжатые указатели на огромных хипах). Если задать нашему тесту 128-байтовое выравнивание, то мы получим:
Running 64-bit HotSpot VM.
Using compressed references with 7-bit shift.
Objects are 128 bytes aligned.
java.util.HashMap
offset size type description
0 12 (assumed to be the object header + first field alignment)
12 4 Set AbstractMap.keySet
16 4 Collection AbstractMap.values
20 4 int HashMap.size
24 4 int HashMap.threshold
28 4 float HashMap.loadFactor
32 4 int HashMap.modCount
36 4 int HashMap.hashSeed
40 1 boolean HashMap.useAltHashing
41 3 (alignment/padding gap)
44 4 Entry[] HashMap.table
48 4 Set HashMap.entrySet
52 76 (loss due to the next object alignment)
128 (object boundary, size estimate)
VM reports 128 bytes per instance
… в то время как VisualVM радостно отрапортует всё те же 69 байт. Eclipse MAT отрапортует все 128 байт, видимо, пронюхав, что все объекты в хипе выровнены на 128. Но его мы тоже сломаем, когда у нас появится @Contended, и тогда такой безобидный классик:
public static class Test2 {
@Contended private int int1;
private int int2;
}
… отрапортуется java-object-layout как:
Running 64-bit HotSpot VM.
Using compressed references with 3-bit shift.
Objects are 8 bytes aligned.
Test8003985.Test2
offset size type description
0 12 (assumed to be the object header + first field alignment)
12 4 int Test2.int2
16 128 (alignment/padding gap)
144 4 int Test2.int1
148 128 (alignment/padding gap)
276 4 (loss due to the next object alignment)
280 (object boundary, size estimate)
VM reports 280 bytes per instance
… в то время как, VisualVM и Eclipse MAT отрапортуют примерно по 24 байта, что, конечно, ЛПИП!
Часть четвёртая. Эпилог.
- Многие offline-тулы врут ввиду проблем с HPROF'ом, и если для HotSpot'а я ещё могу вести бесполезные переговоры, то что делать с остальными VM-ами, непонятно.
- Немногие online-тулы умеют общаться с VM на равных и извлекать из них нужные знания.
- Частенько, обладая знанием о внутренней структуре можно добавлять поля, не меняя размер объектов (ninja style!).
Автор: TheShade