Pull to refresh

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

Reading time 5 min
Views 14K
В предыдущей статье много комментаторов были не согласны в необходимости наличия знаний о размере объектов в 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 потребляет приблизительно на 30% меньше памяти чем объект класса 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Гб. Да да, все опять из-за тех же заголовков, смещений и ссылок. Поэтому иногда может быть полезным содержать большие объемы данных в файлах ресурсов. Но это, конечно, очень сильно зависит от ситуации и это не всегда выход, а иногда и попросту невозможно.

Порядок имеет значение

Допустим у нас есть два массива:
Object[2][1000]
Object[1000][2]

Казалось бы — никакой разницы. Но на самом деле это не так… С точки зрения потребления памяти — разница колоссальна. В первом случае мы имеем 2 ссылки на массив из тысячи элементов. Во втором случае у нас есть тысяча ссылок на массивы c двумя элементами! С точки зрения памяти во втором случае количество потребляемой памяти больше на 998 размеров ссылок. А это около 7кб. Вот так на ровном месте можно потерять достаточно много памяти.

Сжатие ссылок

Существует возможность сократить память, что используется ссылками, заголовками и смещениями в 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 бит. Что это дает нам?
  1. Все объекты у которых есть ссылка, теперь занимают на 4 байта меньше на каждую ссылку.
  2. Сокращается заголовок каждого объекта на 4 байта.
  3. В некоторых ситуациях возможны уменьшенные выравнивания.
  4. Существенно уменьшается объем потребляемой памяти.


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

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

Заключение

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

Articles