Размеры объектов в Java, или о чём нам врут heap dump’ы

от автора

(пост из серии «будни перформанс-инженеров»)

Привет! Наблюдая достаточно много постов про занимаемое объектами пространство в 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 на равных и извлекать из них нужные знания.
  • Всё это не отменяет раскопок внутрях VM’ов в поисках истины.

ссылка на оригинал статьи http://habrahabr.ru/post/164777/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *