Размеры объектов в Java уже обсуждались на Хабре, например, здесь или здесь. Мне бы хотелось подробнее остановиться на размерах многомерных массивов — простая вещь, которая для меня стала неожиданной.
Оптимизируя вычислительный алгоритм по памяти, я наткнулся на то, что при определённых (вполне разумных) входных параметрах создаётся массив float[500][14761][2]. Сколько он может занимать в памяти (на HotSpot 1.6.0_26 32bit, если кому интересно)? Я примерно прикинул, что 500*14 761*2*sizeof(float) = 500*14 761*2*4 = 59 044 000 байт плюс какой-то оверхед. Решив проверить, как на самом деле, я воспользовался Eclipse Memory Analyzer (невероятно волшебная вещь, рекомендую!) и обнаружил, что «Retained Heap» для этого массива составляет 206 662 016 байт! Неплохой оверхед — 350%. Посмотрим, почему так получилось.
Я поискал, что на эту тему пишут в Интернете, и наткнулся на довольно понятную статью на английском «How to calculate the memory usage of a Java array». Вкратце суть изложения такая:
- Всякий Java-объект имеет оверхед 8 байт;
- Java-массив имеет дополнительный оверхед 4 байта (для хранения размера);
- Ссылка имеет размер 4 байта;
- Размер всех объектов выровнен по 8 байт;
- Многомерный массив — это массив ссылок на массивы.
Этого знания достаточно. Итак мы имеем в конце цепочки массивы float[2]. Их размер — это 2 float (по 4 байта) + 12 байт оверхеда — 20 байт. Выравниваем до 8 — получается 24 байта. Всего у нас таких массивов создано в куче 500*14 761 — 7 380 500 штук. Итого они весят 177 132 000 байт.
Затем у нас есть массивы float[14761][] — массивы из 14 761 ссылок на другие массивы. Каждый такой массив занимает 14 761 ссылки (по 4 байта) + 12 байт оверхеда — 59 056 байт (делится на 8 — выравнивать не надо). Всего этих массивов 500 штук, значит они вместе весят 29 528 000 байт.
Наконец, у нас есть собственно тот массив, который мы завели — float[500][][] — массив из 500 ссылок на двумерные массивы. Он занимает 500*4+12 = 2 012, да ещё 4 байта выравнивания — 2 016 байт.
Складываем, что получилось: 177 132 000 + 29 528 000 + 2 016 = 206 662 016 — как раз то число, которое показал Memory Analyzer.
Сразу же пришло в голову очевидное решение: упорядочить измерения массива по возрастанию. Сделаем float[2][500][14761] и алгоритм от этого никак не пострадает. Какой размер получится в этом случае?
- Массивы float[14761]: 14 761*4+12 = 59 056 байт, всего 1 000 таких массивов, итого 59 056 000 байт.
- Массивы float[500][]: 500*4+12 = 2 012, после выравнивания — 2 016, всего 2 таких массива, итого 4 032 байта.
- Массив float[2][][]: 2*4+12 = 20, после выравнивания — 24 байта.
В сумме 59 060 056 байт, оверхед меньше 0.03%, почти 150 мегабайт памяти спасено.
Отсюда следует правило большого пальца: если размеры массива по измерениям хотя бы примерно известны заранее, следует упорядочивать измерения по возрастанию. Кроме того, обязательно используйте анализаторы памяти: узнаете много нового о своей программе.
Автор: lany