43,3
рейтинг
19 декабря 2011 в 22:24

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

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. А теперь давайте разберемся, почему разница настолько огромна.

Из чего же состоит объект?

Прежде чем определять объем потребляемой памяти, следует разобраться, что же JVM хранит для каждого объекта:
  • Заголовок объекта;
  • Память для примитивных типов;
  • Память для ссылочных типов;
  • Смещение/выравнивание — по сути, это несколько неиспользуемых байт, что размещаются после данных самого объекта. Это сделано для того, чтобы адрес в памяти всегда был кратным машинному слову, для ускорения чтения из памяти + уменьшения количества бит для указателя на объект + предположительно для уменьшения фрагментации памяти. Стоит также отметить, что в java размер любого объекта кратен 8 байтам!



Структура заголовка объекта

Каждый экземпляр класса содержит заголовок. Каждый заголовок для большинства JVM(Hotspot, openJVM) состоит из двух машинных слов. Если речь идет о 32-х разрядной системе, то размер заголовка — 8 байт, если речь о 64-х разрядной системе, то соответственно — 16 байт. Каждый заголовок может содержать следующую информацию:
  • Маркировочное слово (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 байт. Хотя это условие и не обязательно.

Группировка полей

Следует также отметить, что JVM проводит предварительную группировку полей объекта. Это значит, что все поля класса размещаются в памяти в определенном порядке, а не так как объявлены. Порядок группировки выглядит так:
  • 1. 8-ми байтовые типы(double и long)
  • 2. 4-х байтовые типы(int и float)
  • 3. 2-х байтовые типы(short и char)
  • 4. Одно байтовые типы(boolean и byte)
  • 5. Ссылочные переменные


Зачем все это?

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

Выводы

Тема памяти в java очень интересна и обширна, когда я начинал писать эту статью, то думал что уложусь в пару примеров с выводами. Но чем дальше и глубже копаешь, тем больше и интересней становится. Вообще, знать как выделяется память для объектов очень полезная вещь, так как поможет Вам сэкономить память, предотвратить подобные проблемы или оптимизировать вашу программу в местах, где это казалось невозможным. Конечно, места где можно использовать такие оптимизации — очень редки, но все же… Надеюсь статья была Вам интересной.
Дмитрий Думанский @doom369
карма
95,5
рейтинг 43,3
Co-Founder в Blynk
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (39)

  • +11
    спасибо, побольше бы таких ява-программеров, которые знают, на чем ездят )
  • +5
    > Так почему же java программисты так мало знают об этой части своего инструмента?
    А вы подумайте хорошенько (-;
    SizeOf не нужен в Java, потому что нет pointer arithmetics. Для решения проблем с памятью рекомендуется использовать профайлер.

    P.S. В тему
    habrahabr.ru/blogs/java/124909/
    • +4
      Согласен, Java на то и Java, чтобы программист не заострял внимание на проблемы с памятью
      • +11
        Yep.

        Дело еще и в том, что однозначно что-нибудь рассчитать сложно. Ответ на вопрос «сколько места в памяти займёт такой-то объект» всегда будет «зависит от JVM и от ситуации». Можно посчитать, сколько «весит» String, но есть ведь String pool например. Можно прикинуть размер массива int, но в 32 и 64-битных JVM int будет разный. И т.п.

        В C(++) sizeof необходим для pointer arithmetics. Чтоб рассчитывать оффсеты и т.п. В Java зачем?

        Если нужно следить за кол-вом свободной памяти:
        docs.oracle.com/javase/1.4.2/docs/api/java/lang/Runtime.html#freeMemory()

        Если следить за memory usage приложения — нужно профайлить в real life usage ситуациях. Никак иначе, и для C(++) подозреваю это тоже справедливо, хотя и в меньшей мере.

        Просто странно как-то это подавать в стиле близком к такому: «вот чудаки джавишные, не знаю сколько памяти юзают. А я вот знаю хак чтоб посчитать — я крут».
        Хак он хак и есть. Not for production, please.
        • +3
          Я с Вами не согласен. Это как не знать как работает процессор. Вы, конечно, можете апеллировать к тому, что никогда не используете напрямую регистры и система все делает за Вас. Но мне кажется, что сущность каждого программиста — изучать что-то новое. Может это и не нужно, но интересно точно.
          • +12
            Как «плавно» перешли от sizeof к «изучать что-то новое». Давайте еще о тайской медицине поговорим…
            Изобретение sizeof != изучение принципов работы JVM.

            И да, мало кто знает сейчас _как именно_ работают различные процессоры, потому что процессоры весьма непросты. Вы всё знаете о конвейерах например? Можете сходу назвать отличия в наборе инструкций x86 процессоров разных, ARM9, ARM11? И т.д и т.п. Вряд-ли.
            Значит ли это, что вы — слабенький программист, который не интересуется ничем «дальше своей песочницы»? Вовсе нет.
            Так о чём собственно речь?

            Есть полно более полезных вещей, достойных изучения, нежели sizeof для Java.
            Sizeof интересен в какой-то степени тоже, но не особо. IMHO.
            • +1
              Отмечу, что лично я уже давно не слежу за спецификой работы процессоров. Для x86 последний «понятный» процессор был третий пень. А потом с каждой иттерацией каждый процессор в нюансах уже другой. Тех же «четвёртых пней» — пруд пруди. И у каждого столько отличий… Следить за этим не будучи инженером в данной области просто невозможно. А ещё AMD…
  • 0
    А я и не говорил, что SizeOf нужен. Я говорил о том, что поголовно люди не могут оценить объем потребляемой памяти java программой. Ведь при определенных знаниях сделать это очень легко.
    • +3
      В Java сложно это сделать, вам уже описали выше почему, плюс ко всему работает GC, так что реальный объем памяти только через профайлинг можно отследить, не говоря уже о том что иерархии классов в Java бывают значительными, сидеть высчитывать их вычислять дело совсем не благодарное, проще запустить профайлер.
      • 0
        Еще и GC бывают разные…
  • +2
    Откуда эта часть — «Структура заголовка объекта»? Хочу пруфы.
    • +3
      Немного отсюда openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
      и отсюда java.sun.com/products/hotspot/docs/whitepaper/Java_HotSpot_WP_Final_4_30_01.html#pgfId=1082734

      Еще была одна ссылка, сечас не могу найти
      • +2
        Вы часто пишите для Java 1.3?

        Я задал свой оригинальный вопрос потому, что в статье написана информация, имеющая в данный момент мало общего с реальностью.
        • 0
          Бросьте линком пожалуйста, если нет трудно.
          • +2
            Ваш первый линк.
            • 0
              Я про то, где можно прочитать про структуру заголовка.
              • +1
                Сейчас заканчиваю топик.
                • 0
                  • –1
                    Не поленился, заглянул в исходники. Вот — hg.openjdk.java.net/jdk6/jdk6/hotspot/file/8389681cd7b1/src/share/vm/oops/markOop.hpp
                    Собственно, мне из этого трудно понять саму структуру и я не могу сказать что значат первые 1 0 0 0 байт, но похоже на инфу для сборщика. То что я вижу явно не противоречит тому, что я написал. И Ваш вывод про magic number несколько поспешен.
  • +10
    По-моему тех кто не знает разницы между

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

    сложно называть программистом… Каким-то другим словом — пожалуйста, но только не программистом…
    • 0
      > сложно называть программистом

      на Java.
    • 0
      А разницу между

      Integer a = 127;
      Integer b = 128;

      ?
      • 0
        Без понятия. Я только изучаю джаву. Но таких тонкостей не знаю пока. Что я вижу, так это автобоксинг который преобразуется компилятором в

        Integer a = new Integer(127);
        Integer b = new Integer(128);

        А какая тут разница я без понятия… Просветите, буду благодарен
        • 0

          Для более эффективного использования памяти, в джаве используются так называемые пулы. Есть строковый пул, Integer pool итд. Когда мы создаем объект не используя операцию new, объект помещается в пул, и в последствии, если мы захотим создать такой же объект (опять не используя new), новый объект создан не будет, а мы просто получим ссылку на наш объект из пула.
          Особенность Integer-пула — он хранит только числа, которые помещаются в тип данных byte: от -128 до 127. Для остальных чисел пул не работает.

          http://habrahabr.ru/blogs/java/111189/
          http://habrahabr.ru/blogs/java/112674/
          и т.п.
          • 0
            Спасибо. Про пулы строк читал, а вот про другие не знал )
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Зачем и правда знать сколько с точностью до байта занимает объект памяти? Достаточно знать, что объекты жрут памяти больше чем примитивы. Если так нужно экономить — используйте Apache Commons primitives, этого достаточно я считаю
  • 0
    > Да, большинству из нас это вообще не нужно и никто в реальных java проектах не будет об этом думать.

    На этой фразе я почувствовал конгитивный диссонанс. Я вроде бы занимаюсь реальным проектом, мне это нужно, я об этом думаю, значит я отношусь к категории «никто»?

    > Иными словами, размеры экземпляров Ваших классов могут отличатся от одной JVM к другой.

    Вот! Это верно, а остальное — шелуха.

    > Зачем и правда знать сколько с точностью до байта занимает объект памяти?

    Чтобы при необходимости оптимизации из нескольких вариантов выбрать более подходящий. Зачем строителю знать вес железобетонной плиты? Чтобы рассчитать размер фундамента и выбрать подходящий кран. Впрочем, строителю не нужна точность до грамма, а жава-программисту до байта (критические случаи CDC и всякие Java-Card не будем вспоминать)

    • 0
      А если есть необходимость оптимизации, не стоит ли тут глянуть в сторону настройки параметров Java Memory Model? Пересмотром размеров eden, old generation и т.д. можно многого добиться.
      • +2
        Java бывает не только на компьютерах.
        Давайте сформулируем общий принцип: не всегда есть доступ к настройкам того окружения в котором будет выполняться твоя программа.
        (в моем случае в тезисе можно смело заменить «не всегда есть доступ» на «нет доступа» — я программирую для телефонов)
      • +4
        Уважаемые Java программисты.
        Не путайте, плиз, Java Memory Model и то как работает Memory Management.
        Это разные вещи. Java Memory Model — не настраивается.
  • +1
    Мне кажется, что акцент в статье неправильно поставлен над тем, сколько занимает объект. Правильнее задумываться, что именно содержится в заголовке объекта и для чего эта информация служит. Например, часто на интервью людям с опытом задаю простой вопрос: для чего в JVM определено два разных байткода: invokevirtual и invokeinterface? Вопрос на самом деле из разряда бонусных: не знаешь так не знаешь, хуже про тебя никто думать не будет. Но показательно, что за многие годы никто пока не ответил.
    • 0
      Погуглил, нашел ответ. Не могу понять, зачем нужно это знание Java программисту.
  • 0
    Oracle разве так и не добавил сжатие заголовок для объектов в 64битовой системе?
    • 0
      Сжимаются указатели на объекты. Заголовок так и остался равен двум 64-битным словам.
  • 0
    я джва года ждал эту статью!
  • 0
    А Вы не пробовали написать sizeof utility? Естественно для внутренних классов JVM нужно будет сделать предрасчет для разных платформ. Я когда-то пробовал, но не хватило терпения.
    На вопрос «для чего»: Для регулирования кол-ва объектов в Cache, когда объекты динамического размера, например содержат разное кол-во содержимых атрибутов, стринги разной длины, и.т.п. Конкретно, мне надо было расчитивать оптимальный размер Cache для платформы датинга, где заполненность профиля может быть разная, и меняться от релиза к релизу из-за добавления новых полей. Было бы полезно, если бы Cache мог бы сам адаптировать свой размер (кол-во профилей) исходя из доступной памяти и среднего размера объектов.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0

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