Сколько памяти занимает массив java
Перейти к содержимому

Сколько памяти занимает массив java

  • автор:

Что из себя представляет массив в языке программирования Java?

Все мы знаем, что язык программирования Java является чисто объектным языком. Все сущности, с которыми нам приходится иметь дело и осуществлять какие-либо манипуляции, являются объектами (за исключением примитивов), в том числе и массивы. То бишь, если говорить совсем уж простыми словами, то любой массив является конкретным объектом в памяти. Java является сильно типизированным языком. Хоть и градация на языки со слабой и сильной типизацией данных является весьма условной, но Java, так или иначе, больше относится к языкам с сильной типизацией. Это ведёт нас к тому, что все данные имеют свой фиксированный тип (или класс, если говорить в терминах ООП). Вот здесь и вся загвоздка! Мне всегда хотелось узнать, каким образом описываются массивы, как некоторые абстрактные сущности, на физическом уровне. Я понимаю, что невозможно найти готовый класс, в котором бы была описана структура массивов, по той простой причине, что данная сущность является одной из фундаментальных (на равне с примивными типами данных) и её реализация спрятана где-то на уровне JVM или в каком-то другом месте. Давайте рассмотрим тривиальный пример. Мы знаем, что размер любого массива является фиксированным и определяется на этапе создания самого массива. Сведения о размере массива хранятся в целочисленной переменной с одноимённым названием length. Сразу же возникает вопрос относительно этого поля. Откуда оно взялось? Где можно проследить всю эту внутреннюю логику (если можно так выразиться)? Идём далее. Создали массив в памяти, при этом сразу же указали его размер. Размер массива соответствует количеству однотипных элементов, которые могут храниться в этом массиве. И тут опять-таки вопрос. По какой логике JVM определяет количество элементов, которое нам необходимо? Точнее не совсем так. Понятное дело, что мы сами указываем размер массива, но разве количество полей для отдельно взятого типа данных не должно быть фиксированным?! Есть ли какой-нибудь код (пусть даже псевдокод), который мог бы хоть немного пролить свет на данный вопрос.

Отслеживать

13.7k 12 12 золотых знаков 43 43 серебряных знака 75 75 бронзовых знаков

Реализация массивов — Java: Массивы

Когда мы говорим про примитивные типы данных, такие как int или double, то, на интуитивном уровне, все довольно понятно. Под каждое значение выделяется некоторое количество памяти в соответствие с типом, в которой и хранится само значение. А как должна выделиться память под хранение массива? И что такое массив в памяти? На уровне хранения понятия «массив» не существует. Массив представляется цельным куском памяти, размер которого вычисляется по формуле «количество элементов» * «количество памяти под каждый элемент». Из этого утверждения есть два интересных вывода:

  • Размер массива — это фиксированная величина. Те динамические массивы, с которыми мы имеем дело, во многих языках реализованы уже внутри языка, а не на уровне железа
  • Все элементы массива имеют один тип и занимают одно и то же количество памяти. Благодаря этому появляется возможность простым умножением (по формуле, описанной ниже) получить адрес той ячейки, в которой лежит нужный нам элемент. Именно это происходит под капотом, при обращении к элементу массива под определенным индексом

Фактически, индекс в массиве — смещение относительно начала куска памяти, содержащего данные массива. Адрес, по которому расположен элемент под конкретным индексом, рассчитывается так: начальный адрес + индекс * количество памяти, занимаемое одним элементом:

// Инициализация массива из пяти элементов типа int // int занимает 4 байта // Общее количество памяти выделенное под массив int * 5 = 4 * 5 = 20 байт int[] numbers = 19, 10, 8, 17, 9>; numbers[3]; // 17 

Адрес элемента, соответствующего индексу 3, вычисляется так: начальный адрес + 3 * 4 (размер типа данных int). Начальный адрес — это адрес ячейки памяти, начиная с которой располагается массив. Он формируется во время выделения памяти под массив. Ниже пример расчета адресов памяти под разные элементы массива numbers:

Рассмотрим еще раз определение массива:

// Первый элемент // Начальный адрес + 4 * 0 = начальный адрес numbers[0]; // 19 // Начальный адрес + 4 * 1 = начальный адрес + 4 // То есть сместились на 4 байта numbers[1]; // 10 // Начальный адрес + 4 * 2 = начальный адрес + 8 // То есть сместились на 8 байт numbers[2]; // 8 // Последний элемент // Начальный адрес + 4 * 4 = начальный адрес + 16 // То есть сместились на 16 байт // И сам элемент занимает 4 байта. В сумме как раз 20 numbers[4]; // 9 

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

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

Размеры объектов в Java уже обсуждались на Хабре, например, здесь или здесь. Мне бы хотелось подробнее остановиться на размерах многомерных массивов — простая вещь, которая для меня стала неожиданной.

Оптимизируя вычислительный алгоритм по памяти, я наткнулся на то, что при определённых (вполне разумных) входных параметрах создаётся массив float[500][14761][2]. Сколько он может занимать в памяти (на HotSpot 1.6.0_26 32bit, если кому интересно)? Я примерно прикинул, что = = плюс какой-то оверхед. Решив проверить, как на самом деле, я воспользовался Eclipse Memory Analyzer (невероятно волшебная вещь, рекомендую!) и обнаружил, что «Retained Heap» для этого массива составляет ! Неплохой оверхед — 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 байта. Всего у нас таких массивов создано в куче — штук. Итого они весят байт.

Затем у нас есть массивы float[14761][] — массивы из ссылок на другие массивы. Каждый такой массив занимает ссылки (по 4 байта) + 12 байт оверхеда — байт (делится на 8 — выравнивать не надо). Всего этих массивов 500 штук, значит они вместе весят байт.

Наконец, у нас есть собственно тот массив, который мы завели — float[500][][] — массив из 500 ссылок на двумерные массивы. Он занимает = , да ещё 4 байта выравнивания — байт.

Складываем, что получилось: = — как раз то число, которое показал Memory Analyzer.

Сразу же пришло в голову очевидное решение: упорядочить измерения массива по возрастанию. Сделаем float[2][500][14761] и алгоритм от этого никак не пострадает. Какой размер получится в этом случае?

  • Массивы float[14761]: = байт, всего таких массивов, итого байт.
  • Массивы float[500][]: = , после выравнивания — , всего 2 таких массива, итого байта.
  • Массив float[2][][]: = 20, после выравнивания — 24 байта.

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

Размер Java объектов

Знаете сколько в памяти занимает строка? Каких только я не слышал ответов на этот вопрос, начиная от «не знаю» до «2 байта * количество символов в строке». А сколько тогда занимает пустая строка? А знаете сколько занимает объект класса Integer? А сколько будет занимать Ваш собственный объект класса с тремя Integer полями? Забавно, но ни один мой знакомый Java программист не смог ответить на эти вопросы… Да, большинству из нас это вообще не нужно и никто в реальных java проектах не будет об этом думать. Но это, ведь, как не знать объем двигателя машины на которой Вы ездите. Вы можете быть прекрасным водителем и даже не подозревать о том, что значат цифры 2.4 или 1.6 на вашей машине. Но я уверен, что найдется мало людей, которые не знакомы со значением этих цифр. Так почему же java программисты так мало знают об этой части своего инструмента?

Integer vs int

Все мы знаем, что в java — everything is an object. Кроме, пожалуй, примитивов и ссылок на сами объекты. Давайте рассмотрим две типичных ситуации:

//первый случай int a = 300; //второй случай Integer b = 301; 

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

sizeOf(int) 

то во втором:

sizeOf(reference) + sizeOf(Integer) 

Забегая вперед скажу — во втором случае количество потребляемой памяти приблизительно в 5 раз больше и зависит от JVM. А теперь давайте разберемся, почему разница настолько огромна.

Из чего же состоит объект?
  • Заголовок объекта;
  • Память для примитивных типов;
  • Память для ссылочных типов;
  • Смещение/выравнивание — по сути, это несколько неиспользуемых байт, что размещаются после данных самого объекта. Это сделано для того, чтобы адрес в памяти всегда был кратным машинному слову, для ускорения чтения из памяти + уменьшения количества бит для указателя на объект + предположительно для уменьшения фрагментации памяти. Стоит также отметить, что в java размер любого объекта кратен 8 байтам!
Структура заголовка объекта
  • Маркировочное слово (mark word) — к сожалению мне так и не удалось найти назначение этой информации, подозреваю что это просто зарезервированная на будущее часть заголовка.
  • Hash Code — каждый объект имеет хеш код. По умолчанию результат вызова метода Object.hashCode() вернет адрес объекта в памяти, тем не менее некоторые сборщики мусора могут перемещать объекты в памяти, но хеш код всегда остается одним и тем же, так как место в заголовке объекта как раз может быть использовано для хранения оригинального значения хеш кода.
  • Garbage Collection Information — каждый java объект содержит информацию нужную для системы управления памятью. Зачастую это один или два бита-флага, но также это может быть, например, некая комбинация битов для хранения количества ссылок на объект.
  • Type Information Block Pointer — содержит информацию о типе объекта. Этот блок включает информацию о таблице виртуальных методов, указатель на объект, который представляет тип и указатели на некоторые дополнительные структуры, для более эффективных вызовов интерфейсов и динамической проверки типов.
  • Lock — каждый объект содержит информацию о состоянии блокировки. Это может быть указатель на объект блокировки или прямое представление блокировки.
  • Array Length — если объект — массив, то заголовок расширяется 4 байтами для хранения длины массива.
Спецификация Java

Известно, что примитивные типы в Java имеют предопределенный размер, этого требует спецификация для переносимости кода. Поэтому не будем останавливаться на примитивах, так как все прекрасно описано по ссылке выше. А что же говорит спецификация для объектов? Ничего, кроме того, что у каждого объекта есть заголовок. Иными словами, размеры экземпляров Ваших классов могут отличатся от одной JVM к другой. Собственно, для простоты изложения я буду приводить примеры на 32-х разрядной Oracle HotSpot JVM. А теперь давайте разберем самые используемые классы Integer и String.

Integer и String

Итак, давайте попробуем подсчитать сколько же будет занимать объект класса Integer в нашей 32-х разрядной HotSpot JVM. Для этого нужно будет заглянуть в сам класс, нам интересны все поля, которые не объявлены как static. Из таких видим только одно — int value. Теперь исходя из информации выше получаем:

Заголовок: 8 байт Поле int: 4 байта Выравнивание для кратности 8 : 4 байта Итого: 16 байт 

Теперь заглянем в класс строки:

 private final char value[]; private final int offset; private final int count; private int hash; 

И подсчитаем размер:

Заголовок: 8 байт Поля int: 4 байта * 3 == 12 байт Ссылочная переменная на объект массива: 4 байта Итого: 24 байта 

Ну и это еще не все… Так как строка содержит ссылку на массив символов, то, по сути, мы имеем дело с двумя разными объектами — объектом класса String и самим массивом, который хранит строку. Это, как бы, верно с точки зрения ООП, но если посмотреть на это со стороны памяти, то к полученному размеру нужно добавить и размер выделенного для символов массива. А это еще 12 байт на сам объект массива + 2 байта на каждый символ строки. Ну и, конечно же, не забываем добавлять выравнивание для кратности 8 байтам. Итого в конечном итоге простая, казалось бы, строка new String(«a») выливается в:

new String() Заголовок: 8 байт Поля int: 4 байта * 3 == 12 байт Ссылочная переменная на объект массива: 4 байта Итого: 24 байта new char[1] Заголовок: 8 байт + 4 байта на длину массива == 12 байт Примитивы char: 2 байта * 1 == 2 байта Выравнивание для кратности 8 : 2 байта Итого: 16 байта Итого, new String("a") == 40 байт 

Важно отметить, что new String(«a») и new String(«aa») будут занимать одинаковое количество памяти. Это важно понимать. Типичный пример использования этого факта в свою пользу — поле hash в классе String. Если бы его не было, то объект строки так или иначе занимал бы 24 байта, за счет выравнивания. А так получается что для этих 4-х байтов нашлось очень достойное применение. Гениальное решение, не правда ли?

Размер ссылки

Немножко хотел бы оговорится о ссылочных переменных. В принципе, размер ссылки в JVM зависит от ее разрядности, подозреваю, что для оптимизации. Поэтому в 32-х разрядных JVM размер ссылки обычно 4 байта, а в 64-х разрядных — 8 байт. Хотя это условие и не обязательно.

Группировка полей
  • 1. 8-ми байтовые типы(double и long)
  • 2. 4-х байтовые типы(int и float)
  • 3. 2-х байтовые типы(short и char)
  • 4. Одно байтовые типы(boolean и byte)
  • 5. Ссылочные переменные
Зачем все это?

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

Выводы

Тема памяти в java очень интересна и обширна, когда я начинал писать эту статью, то думал что уложусь в пару примеров с выводами. Но чем дальше и глубже копаешь, тем больше и интересней становится. Вообще, знать как выделяется память для объектов очень полезная вещь, так как поможет Вам сэкономить память, предотвратить подобные проблемы или оптимизировать вашу программу в местах, где это казалось невозможным. Конечно, места где можно использовать такие оптимизации — очень редки, но все же… Надеюсь статья была Вам интересной.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *