Сколько памяти занимает объект js

Обновлено: 05.07.2024

Низкоуровневые языки программирования (например, C) имеют низкоуровневые примитивы для управления памятью, такие как malloc() и free() . В JavaScript же память выделяется динамически при создании сущностей (т.е., объектов, строк и т.п.) и "автоматически" освобождается, когда они больше не используются. Последний процесс называется сборкой мусора . Слово "автоматически" является источником путаницы и зачастую создаёт у программистов на JavaScript (и других высокоуровневых языках) ложное ощущение, что они могут не заботиться об управлении памятью.

Жизненный цикл памяти

Независимо от языка программирования, жизненный цикл памяти практически всегда один и тот же:

  1. Выделение необходимой памяти.
  2. Её использование (чтение, запись).
  3. Освобождение выделенной памяти, когда в ней более нет необходимости.

Первые два пункта осуществляются явным образом (т.е., непосредственно программистом) во всех языках программирования. Третий пункт осуществляется явным образом в низкоуровневых языках, но в большинстве высокоуровневых языков, в том числе и в JavaScript, осуществляется автоматически.

Выделение памяти в JavaScript

Выделение памяти при инициализации значений переменных

Чтобы не утруждать программиста заботой о низкоуровневых операциях выделения памяти, интерпретатор JavaScript динамически выделяет необходимую память при объявлении переменных:

Выделение памяти при вызовах функций

Вызовы некоторых функций также ведут к выделению памяти под объект:

Некоторые методы выделяют память для новых значений или объектов:

Использование значений

"Использование значений", как правило, означает - чтение и запись значений из/в выделенной для них области памяти. Это происходит при чтении или записи значения какой-либо переменной, или свойства объекта или даже при передаче аргумента функции.

Освобождение памяти, когда она более не нужна

Именно на этом этапе появляется большинство проблем из области "управления памятью". Наиболее сложной задачей в данном случае является чёткое определение того момента, когда "выделенная память более не нужна". Зачастую программист сам должен определить, что в данном месте программы данная часть памяти более уже не нужна и освободить её.

Интерпретаторы языков высокого уровня снабжаются встроенным программным обеспечением под названием "сборщик мусора", задачей которого является следить за выделением и использованием памяти и при необходимости автоматически освобождать более не нужные участки памяти. Это происходит весьма приблизительно, так как основная проблема точного определения того момента, когда какая-либо часть памяти более не нужна - неразрешима (т.е., данная проблема не поддаётся однозначному алгоритмическому решению).

Сборка мусора

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

Ссылки

Большая часть алгоритмов сборки мусора основана на понятии ссылки. В контексте управления памятью объект считается ссылающимся на другой объект, если у первого есть доступ ко второму (неважно - явный или неявный). К примеру, каждый объект JavaScript имеет ссылку на свой прототип (неявная ссылка) и ссылки на значения своих полей (явные ссылки).

В данном контексте понятие "объект" понимается несколько шире, нежели для типичных JavaScript-объектов и дополнительно включает в себя понятие областей видимости функций (или глобальной лексической области)

Сборка мусора на основе подсчёта ссылок

Это наиболее примитивный алгоритм сборки мусора, сужающий понятие "объект более не нужен" до "для данного объекта более нет ни одного объекта, ссылающегося на него". Объект считается подлежащим уничтожению сборщиком мусора, если количество ссылок на него равно нулю.

Пример

Ограничение : циклические ссылки

Основное ограничение данного наивного алгоритма заключается в том, что если два объекта ссылаются друг на друга (создавая таким образом циклическую ссылку), они не могут быть уничтожены сборщиком мусора, даже если "более не нужны".

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

Пример из реальной жизни

Браузеры Internet Explorer версий 6, 7 имеют сборщик мусора для DOM-объектов, работающий по принципу подсчёта ссылок. Поэтому данные браузеры можно легко принудить к порождению систематических утечек памяти (memory leaks) следующим образом:

DOM-элемент "myDivElement" имеет циклическую ссылку на самого себя в поле "circularReference". Если это свойство не будет явно удалено или установлено в null, сборщик мусора всегда будет определять хотя бы одну ссылку на DOM-элемент, и будет держать DOM-элемент в памяти, даже если DOM-элемент удалят из DOM-дерева. Таким образом, если DOM-элемент содержит много данных (иллюстрируется полем "lotsOfData"), то память, используемая под эти данные, никогда не будет освобождена.

Алгоритм "Mark-and-sweep"

Данный алгоритм сужает понятие "объект более не нужен" до "объект недоступен".

Основывается на понятии о наборе объектов, называемых roots (в JavaScript root'ом является глобальный объект). Сборщик мусора периодически запускается из этих roots, сначала находя все объекты, на которые есть ссылки из roots, затем все объекты, на которые есть ссылки из найденных и так далее. Стартуя из roots, сборщик мусора, таким образом, находит все доступные объекты и уничтожает недоступные.

Данный алгоритм лучше предыдущего, поскольку "ноль ссылок на объект" всегда входит в понятие "объект недоступен". Обратное же - неверно, как мы только что видели выше на примере циклических ссылок.

Начиная с 2012 года, все современные веб-браузеры оснащаются сборщиками мусора, работающими исключительно по принципу mark-and-sweep ("пометь и выброси"). Все усовершенствования в области сборки мусора в интерпретаторах JavaScript (генеалогическая/инкрементальная/конкурентная/параллельная сборка мусора) за последние несколько лет представляют собой усовершенствования данного алгоритма, но не новые алгоритмы сборки мусора, поскольку дальнейшее сужение понятия "объект более не нужен" не представляется возможным.

Теперь циклические ссылки - не проблема

В вышеприведённом первом примере после возврата из функции оба объекта не имеют на себя никаких ссылок, доступных из глобального объекта. Соответственно, сборщик мусора пометит их как недоступные и затем удалит.

То же самое касается и второго примера. Как только div и его обработчик станут недоступны из roots, они оба будут уничтожены сборщиком мусора, несмотря на наличие циклических ссылок друг на друга.

Ограничение: некоторые объекты нуждаются в явном признаке недоступности

Хотя этот частный случай и расценивается, как ограничение, но на практике он встречается крайне редко, поэтому, в большинстве случаев, вам не нужно беспокоиться о сборке мусора.

Если вам для каких-то целей потребовалось узнать сколько памяти занимает какой-либо объект в браузере, то инструменты разработки помогут вам это сделать. В Chrome DevTools в разделе Memory можно сделать снимок памяти и посмотреть какие там есть объекты на данный момент времени. Однако, без подготовки найти нужные данные будет совсем не просто.

Процесс поиска нужного объект можно значительно облегчить, если вынести его в глобальную область видимости и изучать только разницу между двумя состояниями (до появления этого объекта и после).

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

Делаем начальный снимок памяти

Теперь переключимся в раздел Console и добавим какой-то тестовый объект.

Создаем объект в консоли

Затем делаем ещё один снимок памяти. Укажем в селекторе, что нужно показать только объекты, созданные между первым и вторым снимком.

Retained size объекта

В списке станет значительно меньше данных. Нас интересует именно Object. И первым же выбранным элементом оказывается наш тестовый объект.

В колонке Retained Size будет указано сколько памяти он занимает. В моём случае это 2488 байт.

Создадим ещё аналогичный объект, но с другими данными.

Создаем другой объект в консоли

Сделаем ещё один снимок экрана и обнаружим, что размер занимаемой памяти теперь значительно уменьшился. Второй объект у меня занимает 736 байт.

Новый объект занимает меньше памяти

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

Грубо оценить эти расходы можно следующим образом:

  • 2 байта на символ строки
  • 8 байт на число
  • 4 байта на булевское значение

Значит мои тестовые объект со случайными значениями полей будут занимать примерно 420-500 байт. Плюс, нужно учесть накладные расходы на хранение структур самих объектов.

Чтобы проверить эту оценку, загрузим 10 тысяч объектов, подобных тем, что я использовал в предыдущих примерах.

Загружаем 10000 объектов

Чтобы значение, обрабатываемое в промисе, не потерялось, так же сохраняем его в глобальную переменную.

Теперь в снимке памяти нас интересует массив.

Массив с тестовыми данными

Итак, загруженные данные (10 тысяч объектов) занимают 4959384 байта, что в пересчете на один объект будет 496 байт. Это значение хорошо вписывается в оценку.

Теперь объект stud будет занимать некоторый размер в памяти. Он содержит несколько данных и больше объектов.

Как узнать, сколько памяти занимает объект stud ? Что-то вроде sizeof() в JavaScript? Было бы действительно здорово, если бы я смог найти это в одном вызове функции, например sizeof(stud) .

Я переустановил код в своем исходном ответе. Я удалил рекурсию и удалил предполагаемые накладные расходы.

Google Chrome Heap Profiler позволяет вам проверять использование памяти объектов.

Вам нужно найти объект в трассировке, которая может быть сложной. Если вы привяжете объект к глобальному окну, его легко найти в режиме листинга "Containment".

В прикрепленном скриншоте я создал объект, называемый "testObj" в окне. Затем я находился в профилировщике (после записи), и он показывает полный размер объекта и все в нем под "сохраненным размером".

Chrome profiler

В приведенном выше скриншоте объект показывает сохраненный размер 60. Я считаю, что единица здесь байтов.

Я просто написал это, чтобы решить аналогичную проблему (ish). Это не совсем то, что вы можете искать, т.е. Не учитывает, как интерпретатор сохраняет объект.

Но, если вы используете V8, он должен дать вам довольно хорошее приближение, поскольку удивительные прототипы и скрытые классы лижут большую часть накладных расходов.

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

Существует модуль NPM для получения объекта sizeof, его можно установить с помощью npm install object-sizeof

Немного поздно на вечеринку, но здесь немного более компактное решение проблемы:

Это хакерский метод, но я дважды пробовал его с разными номерами, и он кажется последовательным.

Что вы можете сделать, это попытаться выделить огромное количество объектов, например, один или два миллиона объектов, которые вы хотите. Поместите объекты в массив, чтобы сборщик мусора не выпустил их (обратите внимание, что это добавит небольшие издержки памяти из-за массива, но я надеюсь, что это не имеет значения, и, кроме того, если вы будете беспокоиться об объектах, находящихся в памяти, вы их где-то храните). Добавьте предупреждение до и после выделения и в каждом предупреждении проверьте, сколько памяти занимает процесс Firefox. Прежде чем открывать страницу с тестом, убедитесь, что у вас есть новый экземпляр Firefox. Откройте страницу, обратите внимание на использование памяти после отображения предупреждения "до". Закройте оповещение, подождите, пока память будет выделена. Вычтите новую память от более старой и разделите ее на количество распределений. Пример:

Я попробовал это на своем компьютере, и у процесса было 48352K памяти, когда было показано предупреждение "before". После выделения Firefox имел 440236 КБ памяти. Для 2миллионных распределений это составляет около 200 байт для каждого объекта.

Я попробовал это снова с 1 миллионом распределений, и результат был похож: 196 байт на объект (я полагаю, дополнительные данные в 2mill были использованы для Array).

Итак, вот хакерский метод, который может вам помочь. JavaScript не предоставляет метод "sizeof" по одной причине: каждая реализация JavaScript отличается. В Google Chrome, например, одна и та же страница использует около 66 байт для каждого объекта (как минимум, из диспетчера задач).

Как известно, JavaScript-движок V8 весьма популярен. Он применяется в браузере Google Chrome, на нём основана платформа Node.js. В материале, подготовленном Мэттом Зейнертом, перевод которого мы публикуем сегодня, приведено девять вопросов, посвящённых особенностям того, как V8 работает с памятью. Каждый вопрос содержит фрагмент кода, который нужно проанализировать и найти ответ, наиболее точно описывающий потребление памяти этим кодом или представленными в нём структурами данных. Ответы на вопросы снабжены комментариями.

image


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

1. Сколько памяти использует каждый элемент массива?

Для ответа на этот вопрос (и на другие подобные вопросы) нужно разделить общую память, потребляемую программой, на длину массива. Здесь мы будем использовать, для указания длины массива, число, представленное переменной MAGIC_ARRAY_LENGTH , равное 1304209. Позже, в комментариях к одному из вопросов, мы остановимся на том, почему здесь используется именно это значение. Пока же отметим, что столь большая длина массива позволяет абстрагироваться от потребления памяти другими частями программ.

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

Варианты ответа

  1. 1 байт
  2. 4 байта
  3. 8 байт
  4. 16 байт
  5. 24 байта
  6. 35 байт
Правильный ответ на этот вопрос — 8 байт. Дело тут в том, что числа в JavaScript представлены 64-битными значениями с плавающей запятой. В байте 8 бит, в результате каждое число занимает 64/8 = 8 байт.

2. Сколько памяти использует каждый элемент массива?

Варианты ответа

В данном случае правильный ответ — 24 байта. В этом примере мы ставим JS-движок в сложное положение. Дело в том, что массив содержит 2 разных типа данных — числа и строки.

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

В результате оказывается, что ссылка на строку — это число, остальные элементы массива — тоже числа. Как их различить? Чем число-ссылка отличается от обычного числа?

Ответ на этот вопрос даёт термин «упакованное значение» («boxed value»). Система упаковывает каждое число в объект и хранит в массиве ссылку на этот объект. Теперь каждый элемент массива может быть ссылкой.

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

  • Ссылку на объект (8 байт)
  • Сам объект, в который упаковано число (16 байт)

Почему для хранения ссылки нужно 8 байт? Помните о том, что системная память похожа на массив? Если используется 32-битная система адресации, то с её помощью можно выразить индексы массива вплоть до 2^32. Если вы храните один байт по каждому индексу массива, это значит, что вы можете оперировать 2^32/(1024*1024*1024) = 4 Гб памяти. Так как большинство компьютеров в наши дни имеют больше чем 4 Гб памяти, для работы с ней приходится использовать 64-битные адреса (для хранения адреса требуется 8 байт). Это — довольно упрощённое пояснение происходящего, однако, оно даёт представление о том, как работает система адресации.

3. Сколько памяти использует каждый элемент массива?

Варианты ответа

В данном случае правильным ответом будет 64 байта. Сколько памяти следует выделить движку V8 для хранения пустого объекта? Это — непростой вопрос. В частности, с учётом того, что предполагается, что объект не будет пустым всегда.

Вот список того, что будет хранить V8 для каждого пустого объекта:

  • Ссылка на скрытый класс (8 байт).
  • 4 пустых ячейки для хранения значений будущих свойств объекта (32 байта).
  • Пустая ячейка для хранения ссылки на дополнительный объект, который будет использовать в том случае, если к исходному объекту будет добавлено более 4-х свойств (8 байт).
  • Пустая ячейка для объекта, который хранит значения для индексов числовых свойств (8 байт).

В результате мы приходим к тому, что для одного элемента массива, состоящего из пустых объектов, понадобится 64 байта, в которые входят 56 байт, которые требуются для хранения объекта в памяти, и 8 байт, которые нужны для хранения ссылки на объект в массиве.

4. Сколько памяти использует каждый элемент массива?

Варианты ответа

Правильным ответом на этот вопрос будет 32 байта. Тут, опять же, в массиве хранятся пустые объекты, но в этот раз мы, для создания объектов, используем функцию-конструктор. V8 может изучить код программы, и понять, что объекты, создаваемые функцией Obj() , ничего не содержат.

  • Ссылка на скрытый класс.
  • Ячейка для хранения ссылки на объект с дополнительными свойствами.
  • Ячейка для ссылки на объект, используемый для хранения индексов числовых свойств.

5. Сколько памяти использует каждый элемент массива?

Варианты ответа

Правильный ответ — 8 байт. Каждый элемент массива должен хранить 64-битную ссылку на текстовое значение, хранящееся в памяти. V8 создаст лишь одну строку, хранящую текст «Hello», а все элементы массива будут ссылаться на неё. Поэтому, если учесть, что у нас имеется достаточно большой массив, размером строки можно пренебречь, и мы придём к тому, что для хранения одного элемента массива V8 понадобится 8 байт.

6. Сколько памяти использует каждый элемент массива?

Варианты ответа

Для хранения одного элемента такого массива требуется 8 байт. Значение true сохраняется в массиве в виде ссылки на объект, так же, как это происходит со строками. В результаты нам снова требуется записывать в элементы массива 64-битные адреса. Значения false , undefined и null обрабатываются похожим образом.

7. Каков общий объём памяти, который потребляет эта программа?

Варианты ответа

Эта программа потребляет 10 Мб памяти. Тут мы храним в массиве немного больше миллиона чисел, каждое из которых занимает 8 байт. В результате можно предположить, что массив займёт примерно 8 Мб памяти. Однако, на самом деле это не так. Элементы в JavaScript-массивы можно добавлять в любое время, но V8 не будет менять размер массива каждый раз, когда вы добавляете в него новый элемент. Для этого, выделяя память под массив, движок оставляет некоторый объём свободного пространства в конце массива.

В предыдущих примерах мы использовали число, представленное переменной MAGIC_ARRAY_LENGTH . Это число находится на границе «запасной» памяти, которая система выделяет массивам. Значение MAGIC_ARRAY_LENGTH равняется 1304209, в то время как 1024*1024 — это 1048576. Однако и в том и в другом случае объём памяти, используемый массивом, будет одним и тем же.

8. Каков общий объём памяти, который потребляет эта программа?

Варианты ответа

В данном случае массиву будет выделено 8 Мб памяти. Так как V8 заранее знает размер массива, система может выделить ему ровно столько памяти, сколько нужно.

9. Каков общий объём памяти, который потребляет эта программа?

Варианты ответа

Этой программе понадобится 2 Мб памяти, так как массив содержит лишь 16-битные целые числа, каждое из которых занимает 2 байта. Таких чисел немного больше миллиона, что означает необходимость в 2-х мегабайтах памяти.

Итоги

Если вы хотите самостоятельно поэкспериментировать со всем тем, чему посвящены вопросы из этого материала, применяя инструменты разработчика Chrome, вы можете воспользоваться следующей конструкцией:


Здесь интересующее нас значение помещено в класс Holder , что упрощает его поиск.


Исследование памяти с помощью инструментов разработчика Chrome

Кстати, автор этого материала говорит, что, проводя эксперименты, он пока не смог до конца понять, как V8 работает в памяти со строками. Если вам удастся это выяснить — уверены, многим будет интересно об этом узнать.

Мне нужно узнать, сколько байт в оперативной памяти занимает определенная коллекция в моем приложении ( ArrayList ). Нужно это для того, чтобы представлять, сколько объектов я могу в нее добавить, не получив OutOfMemory .

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

Конкретно свою проблему я решил, но вопрос остается открытым, ибо мое решение не связано с определением объема памяти, занимаемого объекта.
Итак, я просто решил на практике проверить, сколько же объектов без проблем удержится в памяти на разных моделях телефонов.

java.lang.instrument в Android (Dalvic/ART) не существует.


49.4k 72 72 золотых знака 250 250 серебряных знаков 480 480 бронзовых знаков 20.3k 2 2 золотых знака 25 25 серебряных знаков 49 49 бронзовых знаков
    Самый простой способ — сделать Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() до создания достаточно большой тестовой коллекции и после. Повторить несколько раз, чтобы исключить случайности.

Более сложный — это использовать java.lang.instrumentation package и т.п. для определения размера среднего объекта в коллекции и по нему прикидывать размер коллекции.

Сам по себе ArrayList занимает всего 4(8) байт на элемент (в зависимости от размера указателя, причем даже в 64 битной системе указатели могут быть 4 байтные), то есть основные затраты памяти будут именно на объекты, содержащиеся в ArrayList . Но определение размера объекта в Java вещь сложная, так как не понятно что считать. Если считать только размер самих полей-ссылок — размер объектов будет совсем небольшим, если считать в том числе и все объекты, на которые данные объекты ссылаются, то много объектов могут ссылаться на один общий объект и он посчитается несколько раз (более того все объекты в системе могут ссылаться друг на друга и размер любого объекта можно посчитать равным всем объектам системы). Так что первый способ предпочтительнее.

Читайте также: