В предыдущая статья Многие комментаторы не согласились с необходимостью знаний о размере объектов в Java. Я категорически не согласен с этим мнением и поэтому подготовил несколько практических приемов, которые потенциально могут быть полезны для оптимизации вашего приложения.
Хочется сразу отметить, что не все эти приемы можно применить сразу во время разработки.
Чтобы сделать ситуацию более драматичной, все расчеты и цифры будут представлены для 64-битной JVM HotSpot. Денормализация модели Итак, давайте посмотрим на следующий код:
Теперь денормализуем: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;
}
Объекты классов A и B потребляют одинаковый объем памяти.
Здесь можно сразу сделать 3 вывода:
- Иногда возникают ситуации, когда думаешь – «добавить в класс еще одно поле или сэкономить и посчитать потом на летуЭ» Иногда глупо жертвовать процессорным временем ради экономии памяти, учитывая, что экономии может не быть вообще.
- Иногда мы можем добавить поле, не тратя памяти, и хранить в поле дополнительные или промежуточные данные для вычислений или кэширования (пример хэш-поля в классе String).
- Иногда нет смысла использовать byte вместо int, так как за счет выравнивания разницу все равно можно нивелировать.
Но если в вашем классе поле не должно или не может принимать нулевые значения, смело используйте примитивы.
Потому что очень часто происходит что-то вроде этого: class A {
@NotNull
private Boolean isNew;
@NotNull
private Integer year;
}
Помните, примитивы в среднем занимают в 4 раза меньше памяти.
Замена одного целочисленного поля на int сэкономит 16 байт памяти на каждый объект. А замена одного Long на long занимает 20 байт. Это также снижает нагрузку на сборщик мусора.
В общем, преимуществ очень много.
Единственная цена — отсутствие нулевых значений.
И потом, в некоторых ситуациях, если память действительно нужна, вы можете использовать определённые значения в качестве нулевых значений.
Но это может повлечь за собой дополнительные затраты на доработку логики приложения.
Логическое и логическое значение Эти два типа хотелось бы выделить отдельно.
Все дело в том, что это самые загадочные типы в Java. Поскольку их размер не определен спецификацией, размер логического типа полностью зависит от вашей JVM. Что касается JVM Oracle HotSpot, то все они выделяют под логический тип 4 байта, то есть столько же, сколько и для int. За хранение 1 бита информации вы платите 31 битом в случае логического значения.
Если говорить о булевом массиве, то большинство компиляторов проводят какую-то оптимизацию и в этом случае логическое значение будет занимать по байту на одно значение (ну и про BitSet не забываем).
И, наконец, не используйте логический тип.
Мне сложно представить ситуацию, когда это действительно может понадобиться.
Гораздо дешевле с точки зрения памяти и проще с точки зрения бизнес-логики использовать примитив, который принимал бы 2 возможных значения, а не 3, как в случае с Boolean. Сериализация и десериализация Предположим, у вас есть сериализованная модель приложения, которая занимает 1 ГБ дискового пространства.
И ваша задача — восстановить эту модель в памяти — просто десериализовать ее.
Следует быть готовым к тому, что в зависимости от структуры модели она будет занимать от 2 до 5 ГБ памяти.
Да-да, всё опять из-за тех же заголовков, смещений и ссылок.
Поэтому иногда может быть полезно содержать большие объемы данных в файлах ресурсов.
Но это, конечно, очень сильно зависит от ситуации и не всегда это выход, а иногда просто невозможен.
Порядок имеет значение
Допустим, у нас есть два массива: Object[2][1000]
Object[1000][2]
Казалось бы, нет никакой разницы.
Но на самом деле это не так.
По потреблению памяти разница колоссальная.
В первом случае у нас есть 2 ссылки на массив из тысяч элементов.
Во втором случае у нас есть тысяча ссылок на массивы из двух элементов! Что касается памяти, то во втором случае объем потребляемой памяти на 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 бит. Что это нам дает?
- Все объекты, имеющие ссылку, теперь занимают на 4 байта меньше каждой ссылки.
- Заголовок каждого объекта сокращается на 4 байта.
- В некоторых ситуациях возможны уменьшенные выравнивания.
- Объем потребляемой памяти значительно уменьшается.
- Количество возможных объектов ограничено 2^32. Этот момент вряд ли можно назвать минусом.
Согласитесь, 4 миллиарда объектов — это очень и очень много.
А учитывая, что минимальный размер объекта составляет 16 байт.
- Появляются дополнительные элементы.
затраты на конвертацию JVM-ссылок в нативные и обратно.
Сомнительно, что эти затраты могут оказать какое-то реальное влияние на производительность, учитывая, что это буквально 2 регистровые операции: сдвиг и суммирование.
Подробности можно найти здесь
Мне довелось увидеть некоторые из этих техник на реальных проектах.
И помните, как говорил Дональд Кнут, преждевременная оптимизация — корень всех зол.
Теги: #java #сжатые ссылки #размер объекта #оптимизация #java
-
Зомби На Newsweek.com
19 Oct, 24 -
Особая Порода Мышей
19 Oct, 24 -
Охота На Кремлевского Демона
19 Oct, 24 -
Юмор - Ubuntu В Играх
19 Oct, 24