JAVA / Размер Java объектов. Используем полученные знания

в 11:32, , рубрики: java, оптимизация, метки: ,

В предыдущей статье много комментаторов были не согласны в необходимости наличия знаний о размере объектов в java. Я категорически не согласен с этим мнением и поэтому подготовил несколько практических приемов, которые потенциально могут пригодится для оптимизации в Вашем приложении. Хочу сразу отметить, что не все из данных приемов могут применяться сразу во время разработки. Для придания большего драматизма, все расчеты и цифры будут приводится для 64-х разрядной HotSpot JVM.
Денормализация модели

Итак, давайте рассмотрим следующий код:
class Cursor {
String icon;
Position pos;
Cursor(String icon, int x, int y) {
this.icon = icon;
this.pos = new Position(x, y);
}
}
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
}

А теперь проведем денормализацию:
class Cursor2 {
String icon;
int x;
int y;
Cursor2(String icon, int x, int y) {
this.icon = icon;
this.x = x;
this.y = y;
}
}

Казалось бы — избавились от композиции и все. Но нет. Объект класса Cursor2 потребляет приблизительно на 20% меньше памяти чем объект класса Cursor (по сути Cursor + Position). Такое вот не очевидное следствие декомпозиции. За счет ссылки и заголовка лишнего объекта. Возможно это кажется не важным и смешным, но только до тех пор, пока объектов у Вас мало, а когда счет идет на миллионы ситуация кардинально меняется. Это не призыв к созданию огромных классов по 100 полей. Ни в коем случаем. Это может пригодится исключительно в случае, когда Вы вплотную подошли к верхней границе Вашей оперативной памяти и в памяти у Вас много однотипных объектов.
Используем смещение в свою пользу

Допустим у нас есть 2 класса:
class A {
int a;
}
class B {
int a;
int b;
}

Объекты класса А и B потребляют одинаковое количество памяти. Тут можно сделать сразу 3 вывода:Бывает возникает ситуации когда думаешь — «стоит ли добавить еще одно поле в класс или сэкономить и высчитать его позже на ходу?». Иногда глупо жертвовать процессорным временем ради экономии памяти, учитывая что никакой экономии может и не быть вовсе.

Иногда можем добавить поле не тратя память, а в поле хранить дополнительные или промежуточные данные для вычислений или кеша (пример поле hash в классе String).

Иногда нету никакого смысла использовать byte вместо int, так как за счет выравнивания разница все равно может нивелироваться.

Примитивы и оболочки

Еще раз повторюсь. Но если в Вашем классе поле не должно или не может принимать null значений смело используйте примитивы. Потому что очень уж часто встречается что-то вроде:
class A {
@NotNull
private Boolean isNew;

@NotNull
private Integer year;
}

Помните, примитивы в среднем занимают в 4 раза меньше памяти. Замена одного поля Integer на int позволит сэкономить 16 байт памяти на объект. А замена одного Long на long — 20 байт. Также снижается нагрузка на сборщик мусора. Вообщем масса преимуществ. Единственная цена — отсутствие null значений. И то, в некоторых ситуациях, если память сильно уж нужна, можно использовать определенные значения в качестве null значений. Но это может повлечь доп. расходы на пересмотр логики приложения.
Boolean и boolean

Отдельно хотел бы выделить эти два типа. Все дело в том, что это самые загадочные типы в java. Так как их размер не определен спецификацией, размер логического типа полностью зависит от Вашей JVM. Что касается Oracle HotSpot JVM, то у всех у них под логический тип выделяется 4 байта, то есть столько же сколько и под int. За хранение 1 бита информации Вы платите 31 битом в случае boolean. Если говорить о массиве boolean, то большинство компиляторов проводит некую оптимизацию и в этом случае boolean будут занимать по байту на значение (ну и не забываем про BitSet).
Ну и напоследок — не используйте тип Boolean. Мне трудно придумать ситуацию, где он реально может потребоваться. Гораздо дешевле с точки зрения памяти и проще с точки зрения бизнес логики использовать примитив, который бы принимал 2 возможных значения, а не 3, как в случае в Boolean.
Сериализация и десериализация

Предположим у Вас есть сериализированая модель приложения и на диске она занимает 1 Гб. И у Вас стоит задача восстановить эту модель в памяти — попросту десиарелизировать. Вы должны быть готовы к тому, что в зависимости от структуры модели, в памяти она будет занимать от 2Гб до 5Гб. Да да, все опять из-за тех же заголовков, смещений и ссылок. Поэтому иногда может быть полезным содержать большие объемы данных в файлах ресурсов. Но это, конечно, очень сильно зависит от ситуации и это не всегда выход, а иногда и попросту невозможно.
Сжатие ссылок

Существует возможность сократить память, что используется ссылками, заголовками и смещениями в java объектах. Все дело в том, что еще очень давно при миграции из 32-х разрядных архитектур на 64-х разрядные, многие администраторы, да и просто разработчики заметили падение производительности виртуальных java машин. Мало того, память потребляемая их приложениями при миграции увеличивалась на 20-50% в зависимости от структуры их бизнес модели. Что, естественно, не могло их не огорчать. Причины миграции очевидны — приложения перестали умещаться в доступное адресное пространство 32-х разрядных архитектур. Кто не в курсе — в 32-х разрядных системах размер указателя на ячейку памяти (1 байт) занимает 32 бита. Следовательно максимально доступная память, которую могут использовать 32-х битные указатели — 2^32 = 4294967296 байт или 4 ГБ. Но для реальных приложений объем в 4 ГБ не досягаем в виду того, что часть адресного пространства используется для установленных периферийных устройств, например, видео карты.
Разработчики java не растерялись и появилось такое понятие как сжатие ссылок. Обычно, размер ссылки в java такой же как и в нативной системе. То есть 64 бита для 64-х разрядных архитектур. Это означает, что фактически мы можем ссылаться на 2^64 объектов. Но такое огромное количество указателей излишне. Поэтому разработчики виртуальных машин решили сэкономить на размере ссылок и ввели опцию -XX:+UseCompressedOops. Это опция позволила уменьшить размер указателя в 64-х разрядных JVM до 32 бит. Что это дает нам?Все объекты у которых есть ссылка, теперь занимают на 4 байта меньше на каждую ссылку.

Сокращается заголовок каждого объекта на 4 байта.

В некоторых ситуациях возможны уменьшенные выравнивания.

Существенно уменьшается объем потребляемой памяти.

Но появляются два маленьких минуса:Количество возможных объектов упирается в 2^32. Этот пункт сложно назвать минусом. Согласитесь, 4 млрд объектов очень и очень не мало. А еще учитывая, что минимальный размер объекта — 16 байт...

Появляются доп. расходы на преобразование JVM ссылок в нативные и обратно. Сомнительно, что эти расходы способны хоть как-то реально повлиять на производительность, учитывая что это буквально 2 регистровые операции: сдвиг и суммирование. Детали можно найти тут

Я уверен, что у многих из Вас возник вопрос, если опция UseCompressedOops несет столько плюсов и почти нету минусов, то почему же она не включена по умолчанию? На самом деле, начиная с JDK 6 update 23 она включена по умолчанию, так же как и в JDK 7. А впервые появилась в update 6p.
Заключение

Надеюсь мне удалось Вас убедить. Часть из этих приемов мне довелось повидать на реальных проектах. И помните, как говаривал Дональд Кнут, преждевременная оптимизация — это корень всех бед.


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js