Статьи

Сколько памяти мне нужно

Что такое оставшаяся куча?

Сколько памяти мне нужно? Это вопрос, который вы, возможно, задавали себе (или другим) при создании решения, создании структуры данных или выборе алгоритма. Подойдет ли этот мой график в мою кучу 3G, если он содержит 1 000 000 ребер, и я использую HashMap для его хранения? Могу ли я использовать стандартный API-интерфейс Collections при создании своего собственного решения для кэширования, или они создают слишком много накладных расходов ?

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

Ответ на вопрос в заголовке состоит из нескольких частей. Сначала нам нужно понять, интересуетесь ли вы мелкими или оставшимися размерами кучи.

Мелкая куча проста — она ​​состоит только из кучи, занятой самим объектом . Есть несколько нюансов в том, как его рассчитать, но для этой статьи мы оставим все как есть. Оставайтесь с нами для будущих сообщений на ту же тему.

Сохраненная куча во многих отношениях более интересна. Лишь в редких случаях вас интересует мелкая куча, в большинстве случаев ваш фактический вопрос можно перевести как «Если я удалю этот объект из памяти, сколько памяти теперь может освободить сборщик мусора».

Теперь, как мы все помним, все алгоритмы сборки мусора Java (GC) следуют этой логике:

  1. Есть некоторые объекты, которые ГК считают «важными». Они называются корнями GC и (почти) никогда не выбрасываются. Они, например, в настоящее время выполняют локальные переменные метода и входные параметры, потоки приложения, ссылки из собственного кода и подобные «глобальные» объекты.
  2. Предполагается, что любые объекты, на которые ссылаются эти корни GC, используются и поэтому не удаляются GC. Один объект может ссылаться на другой по-разному в Java, в большинстве случаев объект A хранится в поле объекта B. В этом случае мы говорим «B ссылается на A».
  3. Процесс повторяется до тех пор, пока все объекты, которые могут быть транзитивно достигнуты от корней GC, не посещены и не помечены как «используемые».
  4. Все остальное не используется и может быть выброшено.

Теперь, чтобы проиллюстрировать, как рассчитать оставшуюся кучу, давайте следуем вышеупомянутому алгоритму со следующими примерами объектов:

Чтобы упростить выборку, давайте оценим, что все объекты O1-O4 имеют мелкую кучу 1024B = 1kB. Давайте начнем вычислять оставшиеся размеры этих объектов.

  • O4 не имеет ссылок на другие объекты, поэтому его оставшийся размер равен мелкому размеру 1 КБ .
  • O3 имеет ссылку на O4. Сборщик мусора O3, таким образом, будет означать, что O4 также будет иметь право на сборку мусора, и поэтому мы можем сказать, что объем сохраненной кучи O3 составляет 2 КБ .
  • O2 имеет ссылку на O3. Но теперь важно отметить, что удаление указателя с O2 на O3 не делает O3 подходящим для GC, поскольку у O1 все еще есть указатель на него. Таким образом, оставшаяся куча O2 составляет всего 1 КБ .
  • O1, с другой стороны, является объектом, хранящим все ссылки на этом небольшом графике, поэтому, если мы удалим O1, все на этом графике будет собирать мусор. Таким образом, оставшаяся куча O1 составляет 4 КБ .

Какие последствия это имеет на практике? На самом деле, понимание различий между мелкими и оставшимися размерами кучи позволяет работать с такими инструментами, как профилировщики памяти и анализаторы дампов кучи — например, копание в Eclipse MAT может оказаться невозможным, если вы не знаете, как различить эти два типы измерений размера кучи.

Что такое мелкая куча?

Эта статья является вторым постом в серии, где мы пытаемся ответить на эти вопросы. В последнем посте объясняется разница между сохраняемым и мелким размером объекта. В статье мы также предложили пример того, как рассчитать размер оставшейся кучи структуры данных. В сегодняшней статье мы остановимся на том, что мы называли «простым» в предыдущем посте. А именно — что такое и как измерять мелкую кучу, используемую объектом.

В первом посте мы отбросили много сложностей, заявив, что вычисление мелкого размера кучи легко — оно состоит только из кучи, занятой самим объектом. Но как рассчитать, сколько памяти требует сам объект? Видимо есть формула для этого:

Shallow Heap Size = [reference to the class definition] + space for superclass fields + space for instance fields + [alignment]

Не кажется слишком полезным, а? Давайте попробуем применить формулу, используя следующий пример кода:

1
2
3
4
5
class X {
   int a;
   byte b;
   java.lang.Integer c = new java.lang.Integer();
}
1
2
3
4
class Y extends X {
   java.util.List d;
   java.util.Date e;
}

Теперь мы стремимся ответить на вопрос: сколько мелкого размера кучи требуется для экземпляра Y? Давайте начнем вычислять его, предполагая, что мы находимся на 32-битной архитектуре x86:

В качестве отправной точки — Y является подклассом X, поэтому его размер включает «что-то» из суперкласса. Таким образом, прежде чем вычислять размер Y, мы рассмотрим вычисление мелкого размера X.

Если перейти к вычислениям X, первые 8 байтов используются для ссылки на определение класса. Эта ссылка всегда присутствует во всех объектах Java и используется JVM для определения структуры памяти следующего состояния. У этого также есть три переменных экземпляра — целое число, целое число и байт. Эти переменные экземпляра требуют кучи следующим образом:

  • Байт — это то, чем он должен быть. 1 байт в памяти.
  • int в нашей 32-битной архитектуре требует 4 байта.
  • ссылка на Integer также требует 4 байта. Обратите внимание, что при вычислении оставшейся кучи мы также должны учитывать размер примитива, обернутого в объект Integer, но, поскольку здесь мы вычисляем мелкую кучу, мы используем в наших вычислениях только эталонный размер 4 байта.

Так что же? Малая куча X = 8 байт от ссылки на определение класса + 1 байт (байт) + 4 байта (целое число) + 4 байта (ссылка на целое число) = 17 байт? На самом деле — нет . То, что сейчас входит в игру, называется выравниванием (также называемым отступом). Это означает, что JVM выделяет память кратно 8 байтам, поэтому вместо 17 байт мы бы выделили 24 байта, если бы мы создали экземпляр X.

Если бы вы могли следовать за нами до здесь, хорошо, но теперь мы пытаемся сделать вещи еще более сложными. Мы НЕ создаем экземпляр X, а экземпляр Y. Что это значит — мы можем вычесть 8 байтов из ссылки на определение класса и выравнивание. Это может быть не слишком очевидно с первого взгляда, но — вы заметили, что при расчете небольшого размера X мы не учитывали, что он также расширяет java.lang.Object, как это делают все классы, даже если вы явно не указали его в ваш исходный код? Нам не нужно принимать во внимание размеры заголовков суперклассов, потому что JVM достаточно умен, чтобы проверять его в самих определениях классов, вместо того, чтобы постоянно копировать его в заголовки объектов.

То же самое касается выравнивания — при создании объекта выравнивание выполняется только один раз, а не на границах определений суперкласса / подкласса. Поэтому можно с уверенностью сказать, что при создании подкласса для X вы будете наследовать только 9 байтов от переменных экземпляра.

Наконец, мы можем перейти к исходной задаче и начать вычислять размер Y. Как мы видели, мы уже потеряли 9 байтов для полей суперкласса. Давайте посмотрим, что будет добавлено, когда мы на самом деле создадим экземпляр Y.

  • Заголовки Y, ссылающиеся на определение класса, занимают 8 байтов. Так же, как и с предыдущими.
  • Дата является ссылкой на объект. 4 байта. Легко.
  • Список является ссылкой на коллекцию. Опять 4 байта. Trivial.

Таким образом, в дополнение к 9 байтам из суперкласса у нас есть 8 байтов из заголовка, 2 × 4 байта из двух ссылок (Список и Дата). Общий мелкий размер для экземпляра Y будет 25 байтов, которые выровнены по 32.

Чтобы сделать расчеты более простыми, мы агрегировали их на следующей диаграмме:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Align Align Align Align
Икс объект б с
Y объект б с d е

Что вы можете сделать с этими знаниями? Вместе с навыками расчета размера сохраняемой кучи (описанной в моем недавнем посте ), вы теперь обладаете высочайшей способностью подсчитывать, сколько памяти фактически требуется вашим структурам данных.

Чтобы сделать вещи еще более интересными, мы создали утилиту, которая измеряет размеры как мелкой, так и остаточной кучи для ваших объектов. В самое ближайшее время мы выпустим инструмент для свободного использования. Оставайтесь с нами, подписавшись на нашу ленту Twitter !

Мера, не угадай

То, что выглядит как простая задача, в действительности может стать несколько сложным. Существует множество различных аспектов, которые вы должны учитывать при расчете объема памяти ваших объектов:

  • Нужно ли измерять мелкий или оставшийся размер кучи?
  • Я делаю вычисления для 32 или 64-битной архитектуры?
  • Я работаю на x86, SPARC, POWER или на чем-то, что даже за гранью воображения?
  • Я использую сжатые или несжатые обычные объектные указатели?
  • [введите что-то еще, чего вы боитесь или не до конца понимаете]

Принимая во внимание все эти аспекты при оценке размера ваших структур данных, просто неразумно пытаться уложиться в еще один срок. Поэтому мы пошли дальше и упаковали код, опубликованный Java Champion Heinz Kabutz, в качестве java-агента и предоставили простой способ добавить его в ваше приложение.

Добавление агента дает вам простой способ отследить, сколько памяти занимают ваши структуры данных в вашей реальной среде. И делает это без сложности, представленной альтернативами. В следующих четырех простых шагах вы приступаете к работе и, наконец, понимаете, сколько памяти фактически потребляют ваши драгоценные кэши:

Шаг 1: Загрузите агент. Не волнуйтесь, это всего лишь несколько килобайт.

Шаг 2: Распакуйте загруженный агент. Вы видите, что он упакован вместе с его исходным кодом и примером того, как его использовать. Не стесняйтесь поиграть с кодом.

1
2
3
4
5
nikita-mb:sizeof nikita$ ls -l
total 16
-rw-r--r-- 1 nikita  staff  1696 Aug 28 22:12 build.xml
-rw-r--r--  1 nikita  staff  3938 Aug 28 22:33 sizeofagent.jar
drwxr-xr-x 5 nikita  staff    170 Aug 28 10:44 src

Шаг 3: Поэкспериментируйте с тестовым набором в комплекте. Тестовый пакет в комплекте измеряет ту же структуру данных, которую мы описали в нашем блоге об измерении размера мелкой кучи . Для тех, кто не удосуживается нажимать туда-сюда, вот код снова:

1
2
3
4
5
class X {
   int a;
   byte b;
   java.lang.Integer c = new java.lang.Integer();
}
1
2
3
4
class Y extends X {
   java.util.List d;
   java.util.Date e;
}

Тестовый набор поставляется с тестами Ant для компиляции и запуска образцов. Запустите ant test или ant test-32 если вы используете 32-битную архитектуру. Вы должны увидеть следующий вывод при запуске всех тестов с помощью ant test :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
nikita-mb:sizeof nikita$ ant test
 
Buildfile: /Users/nikita/workspace/sizeof/build.xml
 
init:
 
compile:
 
test32:
 
      [java] java.lang.Object: shallow size=8 bytes, retained=8 bytes
      [java] eu.plumbr.sizeof.test.X: shallow size=24 bytes, retained=40 bytes
      [java] eu.plumbr.sizeof.test.Y: shallow size=32 bytes, retained=48 bytes
 
test64+UseCompressedOops:
 
      [java] java.lang.Object: shallow size=16 bytes, retained=16 bytes
      [java] eu.plumbr.sizeof.test.X: shallow size=24 bytes, retained=40 bytes
      [java] eu.plumbr.sizeof.test.Y: shallow size=32 bytes, retained=48 bytes
 
test64-UseCompressedOops:
 
      [java] java.lang.Object: shallow size=16 bytes, retained=16 bytes
      [java] eu.plumbr.sizeof.test.X: shallow size=32 bytes, retained=56 bytes
      [java] eu.plumbr.sizeof.test.Y: shallow size=48 bytes, retained=72 bytes
 
test:
 
BUILD SUCCESSFUL
Total time: 2 seconds

Из вышеприведенного теста вы можете увидеть, например, что в 32-битной архитектуре небольшая куча Y потребляет 32 байта, а оставшаяся куча — 48 байтов. В 64-битной архитектуре с -XX:-UseCompressedOops небольшой размер увеличивается до 48 байт, а размер кучи сохраняется до 72 байт. Если вас ослепит то, как мы рассчитываем эти числа, то посмотрите, что такое и как рассчитывать мелкие и сохраненные размеры кучи из наших предыдущих публикаций в серии.

Шаг 4. Присоедините агент к собственному Java-приложению. Для этого добавьте -javaagent:path-to/sizeofagent.jar в ваши скрипты запуска JVM. Теперь вы можете измерить потребление мелкой кучи, вызвав MemoryCounterAgent.sizeOf(yourObject) или измерить MemoryCounterAgent.deepSizeOf(yourObject) потребление кучи, вызвав MemoryCounterAgent.deepSizeOf(yourObject) непосредственно в своем коде. Смотрите прилагаемые ant-скрипты и класс eu.plumbr.sizeof.test.SizeOfSample также на случай, если вы eu.plumbr.sizeof.test.SizeOfSample делая это.

Конечно, у вас есть множество альтернатив, особенно в формах профилирования памяти и решениях APM. Но этот маленький агент быстро выполнит свою задачу и почти не требует настройки и обучения. Ну, как минимум, нам было весело играть с ним. Вместо того, чтобы разбираться в нашем продукте .

PS. При написании этой статьи для вдохновения использовались следующие интернет-ресурсы:

И — не забудьте направить свои поздравления за этот код Хайнцу Кабуцу, который первоначально опубликовал его в своем информационном бюллетене для специалистов по Java в марте 2007 года .

Справка: Сколько памяти мне нужно (часть 1) — Что такое куча? Сколько памяти мне нужно (часть 2) — Что такое мелкая куча? Сколько памяти мне нужно (часть 3) — измерьте, не отгадывайте от нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .