Сколько занимает памяти объект пустой класс c
Мне было интересно, какие коллекции сколько съедают дополнительной памяти при хранении объектов. Я провёл замеры накладных расходов для популярных коллекций, предполагающих хранение однотипных элементов (то есть списки и множества) и свёл результаты на общий график. Вот картинка для 64-битной Hotspot JVM (Java 1.6):
А вот для 32-битной Hotspot:
Ниже я расскажу, как проводились измерения, а затем попытаемся разобраться, почему картинки выглядят именно так.
Материалы и методы
Измерения проводились для стандартных коллекций Java из java.util и java.util.concurrent, а также для обычного Java-массива. Коллекции IdentityHashSet и ConcurrentHashSet созданы из соответствующих Map с помощью метода Collections.newSetFromMap(). Все коллекции инициализировались по умолчанию без предварительного указания количества элементов (ну кроме массива, для которого это обязательно), а затем заполнялись тестовыми объектами с помощью метода add (а для массива просто выполнялось присваивание). Было создано около 500 коллекций каждого типа с разным количеством элементов от 1 до 10000. В качестве элементов использовались 10000 различных строк случайной длины, состоящих из случайных букв. В принципе сами элементы влияют лишь на ConcurrentHashSet, да и то незначительно, поэтому графики будут выглядеть похожим образом для любых данных.
После заполнения массивов снимался дамп памяти с процесса и анализировался с помощью Eclipse Memory Analyzer, который очень правильно подсчитал Retained set каждой из коллекций, не включив туда сами объекты, а включив только накладные расходы. Выглядит это, например, так:
Ну а дальше копирование в Excel, нехитрые математические действия и построение графика с небольшой дорисовкой в графическом редакторе.
Видно, что каждая коллекция имеет нижнюю границу накладных расходов и чем больше элементов, тем чаще она оказывается близко к ней. Однако для некоторых коллекций нельзя сказать, что функция накладных расходов сходится к этой границе в математическом смысле. Например, ArrayList хотя всё чаще оказывается у значения 8 байт (для 64bit), но он продолжает прыгать к значению 12 байт при каждом новом выделении памяти.
Интересно, что графики для 32bit и для 64bit очень похожи: для большинства коллекций графики отличаются вдвое кроме двух исключений: ConcurrentSkipListSet и LinkedList. Рассмотрим каждую коллекцию по отдельности и разберёмся, почему для неё график именно такой.
Array
Самый простой вариант — массив, для которого наперёд известно число элементов. В нём на каждый объект хранится ссылка: 4 (8) байт (в скобках значение для 64-битной JVM), кроме того хранится длина массива — int, 4 байта, и дескриптор объекта — 8 (16) байт. Вдобавок каждый объект выравнивается по 8 байтам из-за чего массивы с чётным числом элементов на 32bit теряют по 4 байта. Итог: по 4 (8) байт на объект плюс постоянная от 12 до 24 байт.
Пустой массив занимает 16 (24) байт.
ArrayList
Тут почти то же самое с небольшим отличием: так как заранее число элементов в массиве неизвестно, массив выделяется с запасом (по умолчанию на 10 элементов) и при необходимости расширяется чуть больше, чем в полтора раза:
Поэтому график прыгает до 6 (12) байт. Константа также немного больше: 40 (64) байта, так как помимо массива есть ещё сам объект ArrayList, в котором хранится ссылка на массив, фактический размер списка и количество модификаций (для выкидывания ConcurrentModificationException). Тем не менее, это самый экономный способ хранить однотипные данные, если вы заранее не знаете, сколько их будет.
Конструированный по умолчанию ArrayList без элементов занимает 80 (144) байта.
LinkedList
Для связанного списка картинка похожа на массив: стремится к асимптоте по гиперболе. Для каждого элемента списка создаётся по одному служебному объекту типа java.util.LinkedList.Entry. Каждый из этих объектов содержит по три ссылки (на сам элемент списка, на предыдущий и последующий Entry), при этом из-за выравнивания в 32bit теряется по 4 байта, поэтому в итоге требуется 24 (40) байт на каждый Entry. Константа включает в себя дескриптор объекта LinkedList, головной Entry и ссылку на него, размер списка и количество модификаций и равна 48 (80) байт. Столько же занимает пустой список, так как никакой памяти про запас здесь, конечно, не выделяется.
TreeSet
Вообще все используемые множества построены на основе Map. Отдельная реализация в некоторых случаях могла бы быть несколько компактнее, но общий код, конечно, важнее.
График тоже похож на LinkedList и массив. Для каждого элемента создаётся ветвь дерева java.util.TreeMap.Entry, которая содержит пять ссылок: ключ, значение, родитель, левый и правый ребёнок. Кроме них хранится булева переменная, указывающая цвет ветки, красный или чёрный (см. красно-чёрное дерево). Отдельная булева переменная занимает 4 байта, поэтому запись целиком занимает 32 (64) байта.
Постоянные данных в TreeMap такие: ссылка на компаратор, ссылка на корень дерева (в отличие от головоного Entry в LinkedList корень используется по назначению — ссылается на реальный элемент множества), ссылки на entrySet, navigableKeySet, descendingMap (эти объекты создаются по первому требованию), размер и количество модификаций. С дескриптором объекта TreeMap получается 48 (80) байт. Сам TreeSet добавляет только свой дескриптор и ссылку на TreeMap. В сумме выходит 64 (104) байта. Пустое множество весит столько же. Кстати, расход памяти не зависит от степени сбалансированности дерева.
HashSet
HashSet основан на HashMap, который устроен несколько хитрее, чем TreeMap. Для каждого элемента заводится запись java.util.HashMap.Entry, содержащая ссылки на ключ, значение, следующую Entry (если несколько записей попало в одну ячейку хэш-таблицы), а также само значение хэша. Всего Entry весит 24 (48) байт.
Помимо Entry есть ещё и хэш-таблица, со ссылками на Entry, которая содержит 16 элементов изначально и увеличивается вдвое, когда количество элементов превышает 75% от её размера (75% — это значение loadFactor по умолчанию, его можно указать в конструкторе). То есть при конструировании по умолчанию увеличение таблицы происходит, когда количество элементов превышает 12, 24, 48, 96 и т. д. (2 n *3, последний всплеск на графике — 6144 элемента). Сразу после увеличения таблица в 2/0.75=2.67 раз больше числа элементов, то есть полный расход составляет около 34.67 (69.33) байт на элемент (не считая константы). Непосредственно перед увеличением таблица только в 1/0.75=1.33 раза больше числа элементов, и полный расход составляет 29.33 (58.67) байт на элемент. Заметьте, что расход памяти совершенно не зависит от того, насколько часто происходят коллизии хэшей.
Постоянную компоненту желающие могут вычислить сами, я только скажу, что инициализированный по умолчанию пустой HashSet весит 136 (240) байт.
LinkedHashSet
Почти то же самое, что и в HashSet. Используется java.util.LinkedHashMap.Entry, которая наследует java.util.HashMap.Entry, добавляя две ссылки на предыдущий и следующий элементы, поэтому график на 8 (16) байт выше, чем для HashSet, достигая перед расширением таблицы 37.33 (74.67), а после — рекордных 42.67 (85.33). Константа тоже увеличилась, так как наподобие LinkedList хранится головной Entry, который не ссылается на элемент множества. Свежесозданный LinkedHashSet весит 176 (320) байт.
IdentityHashSet (через newSetFromMap)
IdentityHashMap — очень интересная штука. Она нарушает стандартный контракт Map, сравнивая ключи по ==, а не по equals и используя System.identityHashCode. Ещё она интересна тем, что не создаёт объектов вроде Entry, а просто хранит все ключи и значения в одном массиве (ключи в чётных элементах, значения — в нечётных). В случае коллизии она не создаёт список, а записывает объект в первую свободную ячейку по ходу массива. Благодаря этому достигаются рекордно низкие накладные расходы среди всех множеств.
IdentityHashMap увеличивает размер массива вдвое каждый раз, когда он заполнен больше, чем на 2/3 (в отличие от HashMap этот коэффициент не настраивается). По умолчанию массив создаётся на 32 элемента (то есть размер массива 64). Соответственно, расширение происходит при превышении 21, 42, 85, 170 и т. д. ([2 n /3], последний всплеск на графике — 5461). Перед расширением массив содержит в 3 раза больше элементов, чем ключей в IdentityHashMap, а после расширения — в 6 раз. Таким образом, накладные расходы составляют от 12 (24) до 24 (48) байт на элемент. Пустое множество по умолчанию занимает довольно много — 344 (656) байт, но уже при девяти элементах становится экономичнее всех прочих множеств.
ConcurrentHashSet (через newSetFromMap)
ConcurrentHashMap — первая коллекция, в которой график зависит от самих элементов (а точнее от их хэш-функций). Грубо говоря, это набор фиксированного числа сегментов (по умолчанию их 16), каждый из которых является синхронным аналогом HashMap. Часть бит из модифицированного хэш-кода используется для выбора сегмента, обращение к разным сегментам может происходить параллельно. В пределе накладные расходы совпадают с накладными расходами самого HashMap, потому что java.util.concurrent.ConcurrentHashMap.HashEntry устроен почти так же, как java.util.HashMap.Entry. Увеличение размера сегментов происходит независимо, потому график не поднимается одномоментно: сперва увеличиваются сегменты, в которые попало больше элементов.
Эта коллекция вышла на первое место по начальному размеру — 1304 (2328) байт, потому что сразу же заводится 16 сегментов, в каждом из которых таблица на 16 записей и несколько вспомогательных полей. Однако для 10000 элементов ConcurrentHashSet превышает размер HashSet всего на 0.3%.
ConcurrentSkipListSet
Реализована через ConcurrentSkipListMap и, на мой взгляд, самая сложная из описанных коллекций. Идея алгоритма была описана на Хабре, поэтому здесь я в детали вдаваться не буду. Замечу только, что результирующий объём памяти здесь не зависит от данных, но при этом недетерменирован, так как коллекция инициируется генератором псевдослучайных чисел. На основании следующего псевдослучайного числа принимается решение, добавлять ли индексную запись и на сколько уровней. На каждый элемент обязательно создаётся один объект java.util.concurrent.ConcurrentSkipListMap.Node, который содержит ссылки на ключ, значение и следующий Node, формируя односвязный список. Это даёт 24 (40) байт на каждый элемент. Кроме того, создаётся примерно одна индексная запись (java.util.concurrent.ConcurrentSkipListMap.Index) на каждые два элемента (на первом уровне есть индекс для каждой четвёртой записи, на втором для каждой восьмой и т. д.). Каждая индексная запись весит столько же, сколько и Node (там тоже три ссылки), поэтому в сумме каждый элемент требует около 36 (60) байт. Есть ещё головные записи для каждого уровня (HeadIndex), но их немного (примерно логарифм от числа элементов), поэтому ими можно пренебречь.
В пустом ConcurrentSkipListSet создаётся один HeadIndex и один пустой Node; после конструирования по умолчанию коллекция занимает 112 (200) байт.
Зачем всё это?
Результаты оказались во многом неожиданными и противоречили моим интуитивным представлениям. Так я считал, что конкуррентные коллекции должны занимать заметно больше, чем обычные, а LinkedHashSet должен располагаться где-то между TreeSet и HashSet. Ещё оказалось удивительно, что практически нигде расход памяти не зависит от самих объектов: степень сбалансированности дерева или количество коллизий в хэш-таблицах ни на что не влияют и для неконкуррентных коллекций можно заранее определить размер накладных расходов с точностью до байта. Было интересно покопаться во внутреннем устройстве разных коллекций. Есть ли в этом исследовании конкретная практическая польза — не знаю, пусть каждый решает сам.
Когда мы создаем переменную int var = 5 , все понятно, компьютер берет (выделяет) память 32 бита и записывает туда значение 5 в двоичном виде.
Но что происходит когда мы создаем переменную типа класс? class a = 5 . Что происходит? Сколько байт выделяется под эту переменную?
В первом случае ничего не понятно, так как размер типа int и количество реально выделяемой под него памяти определяется компилятором. Во втором случае ошибка синтаксиса.
class a = 5 - такого в С++ не бывает. class - это ключевое слово и оно не может быть использовано таким образом .
@VTT почему пишут такое? "myVar1 = 25; Указывает компьютеру, что нужно выделить память для переменной myVar1 типа int. Размер памяти выделяемой для нее зависит от самого компьютера. Например на 32-х разрядном компьютере он равен 4 байтам(32 бит)."
myVar1 = 25; можно написать только для уже ранее объявленной переменной. Поэтому не не ясно о каком "указывает компьютеру, что нужно выделить память" вы ведете речь. Нет, ничего подобного это не указывает.
При создании экземпляра класса автоматически запускается конструктор, который может делать с памятью что угодно, хоть всю память зарезервировать.
Объект в куче
При динамическом создании объекта, то есть, выделении памяти в куче, помимо размера самого экземпляра класса можно учесть также и размер указателя на него:
Получается даже, что размер указателя может быть больше самого объекта:
В первом случае - int var = 5; - компилятор выделяет sizeof(int) байтов памяти под переменную var . Это совсем не обязательно 32 бита.
Во втором случае - Class a = 5; - компилятор точно таким же образом выделяет sizeof(Class) байтов под переменную a . Все совершенно единообразно.
Выделение памяти в таких примерах никоим образом не зависит правой части данного объявления, т.е. = 5 никак не влияет на размер выделяемой памяти.
А затем, когда память уже выделена, значение 5 используется в качестве инициализатора для нового объекта. Как именно оно используется - зависит от конкретного типа. В первом случае оно просто заносится в переменную var . А что произойдет во втором случае уже зависит от деталей типа Class . Инициализация в С++ - процесс, описываемый целым набором весьма запутанных правил.
мне было интересно, что может быть размер объекта пустого класса. Она, конечно, могла бы!--4-->не быть 0 байт, так как должно быть возможно ссылаться и указывать на него, как на любой другой объект. Но насколько велик такой объект?
я использовал эту небольшую программу:
вывод, который я получил на компиляторах Visual C++ и Cygwin-g++, был 1 байт! Это было немного удивительно для меня, так как я ожидал, что он будет размером с машинного слова (32 бита или 4 байта).
может ли кто-нибудь объяснить почему размер 1 байт? почему 4 байта? Зависит ли это от компилятора или от машины? Кроме того, может ли кто-нибудь дать более убедительную причину, почему пустой объект класса не будет иметь размер 0 байт?
цитирую стиль и техника C++ Бьярне Страуструпа FAQ, причина, по которой размер не равен нулю, заключается в том, "чтобы гарантировать, что адреса двух разных объектов будут разными."И размер может быть 1, потому что выравнивание здесь не имеет значения, так как на самом деле смотреть не на что.
стандарт утверждает, что все большинство производных объектов имеют sizeof () > = 1:
это действительно деталь реализации. Когда-то давно я думал, что это может быть ноль байтов или тысяча байтов, что это не имеет никакого отношения к спецификации языка. Но, посмотрев на стандарт (раздел 5.3.3), sizeof определяется как всегда возвращающий один или больше, независимо от того, что.
размер наиболее производного класса должен быть больше нуля.
это необходимо, среди прочего, для того, чтобы вы могли обрабатывать массивы объектов и указатели на них. Если вашим элементам было разрешено иметь нулевой размер, то &(array[0]) будет идентично &(array[42]) , что вызовет всевозможные разрушения в ваших циклах обработки.
причина, по которой это может быть не машинное слово, заключается в том, что в нем нет элементов, которые фактически требуют его выравнивания по границе слова (например, целое число). Например, если вы разместите char x; int y; внутри класса мой GCC синхронизирует его с восемью байтами (так как второй int должен быть выровнен в этом реализация.)
даже если не требуется назначать память для пустого класса, но для создания объектов пустых классов компилятор назначает минимальную память, которая может быть назначена, которая составляет 1 байт. Таким образом, компилятор может однозначно различать два объекта одного и того же пустого класса и сможет назначить адрес объекта указателю пустого типа класса.
Я разрабатываю приложение, в котором в настоящее время созданы сотни объектов.
можно ли определить (или приблизить) память, выделенную объектом (экземпляром класса)?
вы можете использовать профилировщик памяти, как
грубый способ может быть в случае, если вы хотите знать, что происходит с конкретным объектом
процесс широкий материал может быть получен, возможно, так
надеюсь, что это помогает ;)
на профилировщик памяти муравьев скажет вам точно, сколько выделяется для каждого объекта/способ/и т. д.
вот связанный пост где мы обсуждали определение размера ссылочных типов.
вы также можете использовать WinDbg и SOS или SOSEX (например, SOS с большим количеством команд и некоторыми существующими улучшенными) расширения WinDbg. Команда, которую вы будете использовать для анализа объекта по определенному адресу памяти !objsize
один очень важный пункт, чтобы помнить это !objsize дает только размер самого класса и не обязательно включает размер агрегатных объектов, содержащихся внутри класса - я понятия не имею, почему он не делает это так, как есть иногда это разочаровывает и вводит в заблуждение.
Я создал 2 предложения функций на веб-сайте Connect, которые просят включить эту возможность в VisualStudio. Пожалуйста, проголосуйте за пункты вы хотели бы видеть их добавлены как хорошо!
EDIT: Я добавляю следующее, чтобы уточнить некоторую информацию из ответа, предоставленного Чарльзом Бретаной:
- в ОП спросил о размере "объекта", а не "класса". Объект является экземпляром класса. Может, ты это имел в виду?
- память, выделенная для объекта, не включает JITted-код. Код JIT живет в своей собственной "куче кода JIT".
- JIT компилирует код только по методу на основе метода-не на уровне класса. Поэтому, если метод никогда не вызывается для класса, он никогда не компилируется JIT и, следовательно, никогда не имеет памяти, выделенной для него в коде JIT Куча.
в стороне, есть около 8 различных куч, которые CLR использует:
- куча загрузчика: содержит структуры CLR и тип system
- высокочастотная куча: статика, MethodTables, FieldDescs, карта интерфейса
- низкочастотная куча: EEClass, ClassLoader и таблицы поиска
- куча заглушек: заглушки для CAS, com-обертки, P/Invoke
- большая куча объектов: выделения памяти, требующие более 85k байты
- GC Heap: пользователь выделил память кучи частной для приложения
- куча кода JIT: память, выделенная mscoreee (механизм выполнения) и компилятором JIT для управляемого кода
- Process / Base Heap: взаимодействие/неуправляемые выделения, собственная память и т. д.
чтобы получить общее представление о распределении памяти в приложении, используйте следующую команду sos в WinDbg
обратите внимание, что !dumpheap дает только байты самого типа объекта и не включает байты любых других типов объектов, на которые он может ссылаться.
каждому "классу" требуется достаточно памяти для хранения всего его JIT-скомпилированного кода для всех его членов, которые были вызваны средой выполнения (хотя, если вы не вызываете метод в течение некоторого времени, среда CLR может освободить эту память и повторно jit ее снова, если вы вызовете ее снова. плюс достаточно памяти для хранения всех статических переменных, объявленных в классе. но эта память выделяется только один раз для каждого класса, независимо от того, сколько экземпляров класса, который вы создаете.
для каждого экземпляра класс, который вы создаете (и не был собран мусор) вы можете приблизить объем памяти, добавив использование памяти каждой объявленной переменной на основе экземпляра. (поле)
ссылочные переменные (ссылки на другие объекты) занимают 4 или 8 байт (32/64 бит ОС ?) int16, Int32, Int64 принимают 2,4, или 8 байт, соответственно.
строковая переменная занимает дополнительное хранилище для некоторых элементов метаданных (плюс размер указателя адреса)
кроме того, каждый ссылочная переменная в объекте также может рассматриваться как "косвенно" включающая память, занятую в куче объектом, на который она указывает, хотя вы, вероятно, захотите считать эту память принадлежащей этому объекту, а не переменной, которая ссылается на нее.
Если вы можете - сериализовать его!
существует академический вопрос каков размер объекта во время выполнения? и это интересно, но это может быть правильно ответил только профилировщик, который подключен к запущенному процессу. Я провел довольно много времени, глядя на это недавно и определил, что нет общего метода, который был бы точным и достаточно быстрым, чтобы вы когда-либо хотели использовать его в производственной системе. Простые случаи, такие как массивы числовых типов, имеют простые ответы, но за их пределами это лучший ответ будет Не пытайся разобраться. Зачем тебе это знать? Имеется ли другая информация, которая могла бы служить той же цели?
в моем случае я хотел ответить на этот вопрос, потому что у меня были различные данные, которые были полезны, но могли быть отброшены, чтобы освободить ОЗУ для более важных служб. Плакат "мальчики" являются Отменить Стек и кэш.
В Конце Концов Я пришел к выводу, что правильный способ управления размером стека отмены и кэша состоял в том, чтобы запросить объем доступной памяти (это 64-разрядный процесс, поэтому можно с уверенностью предположить, что все это доступно), а затем разрешить добавлять больше элементов, если есть достаточно большой буфер ОЗУ и требовать удаления элементов, если ОЗУ работает на низком уровне.
использование указателя объекта в качестве массива может использоваться для доступа к i1 следующим образом?
другие вопросы на SO, похоже, предполагают, что приведение структуры к указателю будет указывать на первый член для POD-типов. Как это отличается для классов с конструкторами, если вообще? Также каким образом это отличается для non-POD типы?
в памяти, следовательно, вышеуказанный класс будет выложен следующим образом?
почти. Вы бросили на объект* и забыли взять адрес. Давайте переспросим следующим образом:
вы должны быть очень осторожны с такими предположениями. Поскольку вы определили структуру, это должно быть верно в любом компиляторе, с которым вы, вероятно, столкнетесь. Но все виды других свойств объекта (которые вы, возможно, опустили в своем примере), как говорили другие, сделают его не-POD и могут (возможно, зависящим от компилятора способом) сделать вышеизложенное утверждение неверно.
в любом случае, вы должны избегать такого рода вещи, если это возможно. Даже если сейчас это работает нормально, если вы (или кто-то еще, кто не понимает, что вы делаете этот трюк) когда-либо изменит порядок структуры или добавит новые поля, этот трюк потерпит неудачу во всех местах вы использовали его, что может быть трудно найти.
ответ на ваше редактирование: если это все ваше определение класса, и вы используете один из основных компиляторов с параметрами по умолчанию и работаете на процессоре x86, то да, вы, вероятно, угадали правильный макет памяти. Но выбор компилятора, параметров компилятора и другой архитектуры процессора может легко аннулировать ваши предположения.
классы без виртуальных членов и без наследования выложены в памяти так же, как структуры. Но, когда вы начинаете получать уровни наследования, все может стать сложным, и может быть трудно понять, какой порядок вещей в памяти (особенно множественное наследование).
когда у вас есть виртуальные члены, у них есть" vtable " в памяти, которая содержит указатели на фактическую функцию, которая создается на основе иерархии наследования класс.
суть в следующем: не получить доступ к классам таким образом, если вы можете избежать этого (а также не memset их или memcpy их). Если вы должны это сделать (почему?) затем позаботьтесь о том, чтобы точно знать, как ваши объекты класса будут в памяти, и будьте осторожны, чтобы избежать наследования.
разница в том, что этот трюк действителен только для типов POD. Вот, собственно, и все. Стандарт указывает, что это приведение допустимо для типа POD, но не дает никаких гарантий относительно того, что происходит с типами без POD.
Это действительно зависит от компилятора, или, скорее, это зависит от компилятора, чтобы определить макет памяти.
например, сочетание открытых, частных и защищенных переменных-членов может быть выложено таким образом, чтобы каждый тип доступа был непрерывным. Или производные классы могут иметь переменные-члены, чередующиеся с неиспользуемым пространством в супер классе.
дела ухудшаются с виртуальным наследованием, где фактически унаследованные базовые классы могут быть выложены в любом месте в памяти, выделенной для этого конкретного экземпляра.
POD отличается тем, что он должен быть совместим с C.
обычно не имеет значения, имеет ли класс конструктор: важно, имеет ли класс какие-либо виртуальные методы. Для деталей, google для "vtable" и "vptr".
Объект в стеке
В программе выделяется столько байт, сколько требуется для хранения данных экземпляра данного класса. Например, объект пустого класс займёт один байт, если в нём хранится int , то его размер прибавится к размеру объекта класса. Вот интересный код для исследования этих свойств:
Результат выполнения с моим компилятором:
Меняя число переменных можно заметить, что поля с модификатором static не влияют на размер выделенной памяти, что и логично, ведь она не относится к конкретным экземплярам класса.
2 ответа 2
Читайте также: