Размеры массивов в Java

    Размеры объектов в 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 7617 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 мегабайт памяти спасено.

    Отсюда следует правило большого пальца: если размеры массива по измерениям хотя бы примерно известны заранее, следует упорядочивать измерения по возрастанию. Кроме того, обязательно используйте анализаторы памяти: узнаете много нового о своей программе.
    Поделиться публикацией

    Комментарии 24

      +1
      Спасибо! Познавательно.

        +1
        оо! Познавательно. Спасибо. Полез знакомиться с Memory Analyzer =)
          +14
          Если уж речь заходит о производительности и экономии памяти, не будет ли лучше использовать развернутый одномерный массив? Плюсы: минимальное потребление памяти, получение элемента за одно чтение из памяти.
            0
            Не знаю как в джаве (хотя если 32 бит, так как иначе-то), но в плюсах больше чем ана гигабайт-два выделить массив неполучится — фрагментация адресного пространства.

            А вот 30 массивов по 100 мб (условно) — можно.
              +4
              А что случится, если я выделю на С++ больше 2 Гб в 64 битном приложении?
                0
                exception из new вывалится.
                0
                Особенность Винды? Сорри — реально не имел таких проблем на Линуксе.
                Можно хоть террабайт выделить — и отработает. Вот когда начнете инициализировать и реально пойдет выделение страниц тогда будет своп…
                +2
                Да, можно. Хотя потребление изменится уже несущественно, а код может стать значительно менее читаемым из-за вычисления смещений. Можно ещё написать простенький класс (или найти готовый), реализующий это для массивов произвольной размерности, хотя тогда будут накладные расходы на вызовы функций. В каждом конкретном случае рецепт свой. К примеру, изменения в производительности в моём случае будут эфемерны.
                  0
                  Отвечу на первую часть: вычисление смещений в минимальном варианте заворачиваются в final методы, которые на-ура будут заинлайнены любым компилятором. Аналогично с классом.
                +7
                Сразу же пришло в голову очевидное решение: упорядочить измерения массива по возрастанию… алгоритм от этого никак не пострадает.

                Вообще говоря, может измениться locality of reference. Она лучше, когда измерения идут в том же порядке, в котором вложены циклы.
                  0
                  Правильное замечание. В моём случае производительность не была камнем преткновения и сколь-нибудь значимо не изменилась.
                  +1
                  А не проще одномерный массив использовать и арифметику на индексах?
                    0
                    по крайней мере быстрее — точно
                    +2
                    По поводу использовании памяти в яве есть очень, очень хорошая презентация.
                      0
                      Если быть точным, заголовок объекта — это два машинных слова, соответственно, на 64-битной Java это будет уже не 8, а 16 байт.
                      Аналогично, заголовок массива — три машинных слова, на 64-битной Java — 24 байта.
                      Рекомендую также по теме вот эту презентацию. Один из разработчиков Twitter делится своим опытом в части модели памяти Java, сборки мусора и оптимизации.
                        0
                        Вот спасибо! Кратко, лаконично и полезно — в избранное.
                          –1
                          Массивы float[14761]: 14 761*4+12 = 59 056 байт, всего 1 000 таких массивов, итого 59 056 000 байт.
                          Скорректируй их всего 500
                            0
                            1000. Два по 500.
                              0
                              Ошибся я, извините
                            +2
                            Вообще-то проблема только в том что последний массив — это float[2]
                            Если бы там был float[100], то итоговый суммарный оверхед был бы не 350%, а доли процента.

                            Так что уточненная мораль сей истории состоит не в том, чтобы упорядочивать вложенные массивы по возрастанию, а в том, чтобы не создавать очень много очень мелких обьектов, когда есть возможность вместо этого создать обьектов не много, но крупных.
                              0
                              Ну не доли процента, а минимум (416/400-1)*100% = 4%.
                                0
                                Согласен, большая часть уходит на выравнивание мелких объектов
                                0
                                У вас еще перформанс должен поменяться если у вас там матиматика с обходом массива…

                                Вообще самый первый индекс должен быть максимальным — наилучшее использование кэша. Первый — тот который по строке бежит я имею ввиду.
                                  0
                                  а если его представить как одномерный?
                                  вычисления от этого не пострадают?

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое