Размер Java объектов. Используем полученные знания

    В предыдущей статье много комментаторов были не согласны в необходимости наличия знаний о размере объектов в java. Я категорически не согласен с этим мнением и поэтому подготовил несколько практических приемов, которые потенциально могут пригодится для оптимизации в Вашем приложении. Хочу сразу отметить, что не все из данных приемов могут применяться сразу во время разработки. Для придания большего драматизма, все расчеты и цифры будут приводится для 64-х разрядной HotSpot JVM.

    Денормализация модели

    Итак, давайте рассмотрим следующий код:
    class Cursor {
        String icon;
        Position pos;
        Cursor(String icon, int x, int y) {
             this.icon = icon;
             this.pos = new Position(x, y);
        }
    }
    class Position {
        int x;
        int y;
        Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    

    А теперь проведем денормализацию:
    class Cursor2 {
        String icon;
        int x;
        int y;
        Cursor2(String icon, int x, int y) {
            this.icon = icon;
            this.x = x;
            this.y = y;
        }
    }
    

    Казалось бы — избавились от композиции и все. Но нет. Объект класса Cursor2 потребляет приблизительно на 30% меньше памяти чем объект класса Cursor (по сути Cursor + Position). Такое вот не очевидное следствие декомпозиции. За счет ссылки и заголовка лишнего объекта. Возможно это кажется не важным и смешным, но только до тех пор, пока объектов у Вас мало, а когда счет идет на миллионы ситуация кардинально меняется. Это не призыв к созданию огромных классов по 100 полей. Ни в коем случаем. Это может пригодится исключительно в случае, когда Вы вплотную подошли к верхней границе Вашей оперативной памяти и в памяти у Вас много однотипных объектов.

    Используем смещение в свою пользу

    Допустим у нас есть 2 класса:
    class A {
        int a;
    }
    class B {
        int a;
        int b;
    }
    

    Объекты класса А и B потребляют одинаковое количество памяти. Тут можно сделать сразу 3 вывода:
    • Бывает возникает ситуации когда думаешь — «стоит ли добавить еще одно поле в класс или сэкономить и высчитать его позже на ходу?». Иногда глупо жертвовать процессорным временем ради экономии памяти, учитывая что никакой экономии может и не быть вовсе.
    • Иногда можем добавить поле не тратя память, а в поле хранить дополнительные или промежуточные данные для вычислений или кеша (пример поле hash в классе String).
    • Иногда нету никакого смысла использовать byte вместо int, так как за счет выравнивания разница все равно может нивелироваться.


    Примитивы и оболочки

    Еще раз повторюсь. Но если в Вашем классе поле не должно или не может принимать null значений смело используйте примитивы. Потому что очень уж часто встречается что-то вроде:
    class A {
    	@NotNull
    	private Boolean isNew;
    
    	@NotNull
    	private Integer year;
    }
    

    Помните, примитивы в среднем занимают в 4 раза меньше памяти. Замена одного поля Integer на int позволит сэкономить 16 байт памяти на объект. А замена одного Long на long — 20 байт. Также снижается нагрузка на сборщик мусора. Вообщем масса преимуществ. Единственная цена — отсутствие null значений. И то, в некоторых ситуациях, если память сильно уж нужна, можно использовать определенные значения в качестве null значений. Но это может повлечь доп. расходы на пересмотр логики приложения.

    Boolean и boolean

    Отдельно хотел бы выделить эти два типа. Все дело в том, что это самые загадочные типы в java. Так как их размер не определен спецификацией, размер логического типа полностью зависит от Вашей JVM. Что касается Oracle HotSpot JVM, то у всех у них под логический тип выделяется 4 байта, то есть столько же сколько и под int. За хранение 1 бита информации Вы платите 31 битом в случае boolean. Если говорить о массиве boolean, то большинство компиляторов проводит некую оптимизацию и в этом случае boolean будут занимать по байту на значение (ну и не забываем про BitSet).
    Ну и напоследок — не используйте тип Boolean. Мне трудно придумать ситуацию, где он реально может потребоваться. Гораздо дешевле с точки зрения памяти и проще с точки зрения бизнес логики использовать примитив, который бы принимал 2 возможных значения, а не 3, как в случае в Boolean.

    Сериализация и десериализация

    Предположим у Вас есть сериализированая модель приложения и на диске она занимает 1 Гб. И у Вас стоит задача восстановить эту модель в памяти — попросту десериализовать. Вы должны быть готовы к тому, что в зависимости от структуры модели, в памяти она будет занимать от 2Гб до 5Гб. Да да, все опять из-за тех же заголовков, смещений и ссылок. Поэтому иногда может быть полезным содержать большие объемы данных в файлах ресурсов. Но это, конечно, очень сильно зависит от ситуации и это не всегда выход, а иногда и попросту невозможно.

    Порядок имеет значение

    Допустим у нас есть два массива:
    Object[2][1000]
    Object[1000][2]
    

    Казалось бы — никакой разницы. Но на самом деле это не так… С точки зрения потребления памяти — разница колоссальна. В первом случае мы имеем 2 ссылки на массив из тысячи элементов. Во втором случае у нас есть тысяча ссылок на массивы c двумя элементами! С точки зрения памяти во втором случае количество потребляемой памяти больше на 998 размеров ссылок. А это около 7кб. Вот так на ровном месте можно потерять достаточно много памяти.

    Сжатие ссылок

    Существует возможность сократить память, что используется ссылками, заголовками и смещениями в java объектах. Все дело в том, что еще очень давно при миграции из 32-х разрядных архитектур на 64-х разрядные, многие администраторы, да и просто разработчики заметили падение производительности виртуальных java машин. Мало того, память потребляемая их приложениями при миграции увеличивалась на 20-50% в зависимости от структуры их бизнес модели. Что, естественно, не могло их не огорчать. Причины миграции очевидны — приложения перестали умещаться в доступное адресное пространство 32-х разрядных архитектур. Кто не в курсе — в 32-х разрядных системах размер указателя на ячейку памяти (1 байт) занимает 32 бита. Следовательно максимально доступная память, которую могут использовать 32-х битные указатели — 2^32 = 4294967296 байт или 4 ГБ. Но для реальных приложений объем в 4 ГБ не досягаем в виду того, что часть адресного пространства используется для установленных периферийных устройств, например, видео карты.
    Разработчики java не растерялись и появилось такое понятие как сжатие ссылок. Обычно, размер ссылки в java такой же как и в нативной системе. То есть 64 бита для 64-х разрядных архитектур. Это означает, что фактически мы можем ссылаться на 2^64 объектов. Но такое огромное количество указателей излишне. Поэтому разработчики виртуальных машин решили сэкономить на размере ссылок и ввели опцию -XX:+UseCompressedOops. Это опция позволила уменьшить размер указателя в 64-х разрядных JVM до 32 бит. Что это дает нам?
    1. Все объекты у которых есть ссылка, теперь занимают на 4 байта меньше на каждую ссылку.
    2. Сокращается заголовок каждого объекта на 4 байта.
    3. В некоторых ситуациях возможны уменьшенные выравнивания.
    4. Существенно уменьшается объем потребляемой памяти.


    Но появляются два маленьких минуса:
    • Количество возможных объектов упирается в 2^32. Этот пункт сложно назвать минусом. Согласитесь, 4 млрд объектов очень и очень не мало. А еще учитывая, что минимальный размер объекта — 16 байт...
    • Появляются доп. расходы на преобразование JVM ссылок в нативные и обратно. Сомнительно, что эти расходы способны хоть как-то реально повлиять на производительность, учитывая что это буквально 2 регистровые операции: сдвиг и суммирование. Детали можно найти тут

    Я уверен, что у многих из Вас возник вопрос, если опция UseCompressedOops несет столько плюсов и почти нету минусов, то почему же она не включена по умолчанию? На самом деле, начиная с JDK 6 update 23 она включена по умолчанию, так же как и в JDK 7. А впервые появилась в update 6p.

    Заключение

    Надеюсь мне удалось Вас убедить. Часть из этих приемов мне довелось повидать на реальных проектах. И помните, как говаривал Дональд Кнут, преждевременная оптимизация — это корень всех бед.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 40

      +1
      Ну и напоследок — не используйте тип Boolean. Мне трудно придумать ситуацию, где он реально может потребоваться

      Здесь можно поспорить — Boolean хорошо использовать, когда нужно именно три состояния, например, True/False/Not Set и Yes/No/Don't Know.
        +4
        Ну и напоследок — не используйте тип Boolean. Мне трудно придумать ситуацию, где он реально может потребоваться
        Коллекции
          +1
          Да, кстати, любой Generic будет работать только с Boolean, Long, Integer и т.д., т.е. не с примитивом
            0
            + Boolean нужен при использовании рефлексии
            0
            Вы храните в коллекциях Boolean? Сомнительно.
              +1
              Иногда приходится.
                0
                Возможно, если у вас в коллекции действительно много значений и вы не противитесь сторонним библиотекам, есть смысл попробовать trove.
                0
                Например java.util.Collections.newSetFromMap(Map<E,Boolean> map) как раз таки требует мапу с Boolean.
              +1
              Если Вам нужно хранить 3 состояния, то это уже не boolean. Для этого отлично подойдут перечисления.
                0
                Мне так не кажется — зачем создавать дополнительные сущности, если имеющиеся идеально подходят? Тем более что при правильном использовании Boolean'а в системе находятся только два инстанца. Откуда в таком случае взяться проблемам с памятью?
                  0
                  Потому что null это не «Not Set» и null это не Don't Know.

                  >> при правильном использовании Boolean'а в системе находятся только два инстанца
                  Два инстанса примитива. Boolean это прежде всего объект и помимо значения value он как и каждый объект содержит заголовок + возможные выравнивания для кратности 8 байтам + Вы должны хранить ссылку на этот объект. Согласитесь, это не мало для того чтобы запомнить 1 бит.
                    +1
                    Нет, два инстанца именно Boolean'а. Соответсвенно оверхеда на заголовок + возможные выравнивания для кратности 8 байтам нет. Согласен, есть оверхед на ссылку.

                    Потому что null это не «Not Set» и null это не Don't Know.
                    Всё зависит от того какое логическое поределение мы ему дадим. 2+2 != 4 в кольце вычетов по модулю 3, потому что так определено сложение. Также мы можем определить и использование Boolean'а в зависимости от задачи.
                      0
                      Но заметьте — при использовании Enum'а, тоже будет оверхед на ссылку.
                        0
                        Будет не только оверхед на ссылку, но и оверхед на поле name внутри перечисления. Но в случае перечисления мы всегда уверены что будет создан только один объект перечисления. В случае же Boolean мы не можем этого гарантировать, так как инициализация этого поля может идти через new Boolean и осуществляется в месте где мы не можем ничего изменить.
                        0
                        >>Нет, два инстанца именно Boolean'а
                        На самом деле все будет зависеть от того как будет создаваться объект булеана.
                        Если через Boolean a = new Boolean(false); то прав я.
                        Если через Boolean a = false; то правы Вы.
                          0
                          Под правильным использованием Boolean'а я и имел ввиду отсутствие таких конструкций =)
                            0
                            Тогда уж правильнее всего:
                            Boolean a = Boolean.FALSE;
                            Что и сделает в итоге вторая строка через valueOf. А первая строка вообще как бы и не рекомендуется в явадоке, как помнится.
                  +5
                  Немогу не поделится ссылкой (http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CC0QFjAA&url=http%3A%2F%2Fdomino.research.ibm.com%2Fcomm%2Fresearch_people.nsf%2Fpages%2Fsevitsky.pubs.html%2F%24FILE%2Foopsla08%2520memory-efficient%2520java%2520slides.pdf&ei=v3QyT4yiIeXm4QTwt5ySBQ&usg=AFQjCNEPtILphv6DOcMj3D4qZLKPmBkFxw&sig2=MNFjDK7kl4nVJ9Qbhagosg) на ужасы съедамой объектами памяти, большая часть презентации — про то насколько страшен HashSet.

                  Для себя заметил в последнем проекте что когда пытаешься писать быстрый код, используя кэши и маппинг чтобы не лазить лишний раз в хранилище данных, в реальности проблемы всплывают не с производительностью, а как раз с нехваткой памяти и частыми GC, выгружающими по 1-2 Гб из heap'a, приходится писать свои велосипеды и «низкоуровневый» код.
                    +3
                    Вот так ссылка смотрится лучше.
                      +5
                      Да я рад что с отрицательной кармой хотя бы главная страница открывается, а большего и так нельзя вообще ничего сделать)
                      0
                      Да, согласен. Как только модель становится большой и в кеше начинает хранится много объектов — память начинает играть очень заметную роль.
                        0
                        Спасибо за ссылку.
                        Хороший дизайн всегда идет в ущерб памяти и производительности. Благо на данный момент это дешевые ресурсы: память стоит копейки по сравнению с разработкой высого оптимизированного кода. Другое дело десктопы и устройства с ограниченной памятью типа смартфонов и планшетников… Видимо поэтому java заняла прочное место на рынке серверов и совсем угасла в десктопной среде.
                      • UFO just landed and posted this here
                        • UFO just landed and posted this here
                            +1
                            В спецификации где-то об этом сказано? Если нет, то «Объекты класса А и B потребляют одинаковое количество памяти. „ вовсе не факт.
                            • UFO just landed and posted this here
                                0
                                Да, из-за выравнивания. В Oracle и Open JDK это действительно так. Не думаю что остальные отошли от них.
                                  +3
                                  Не парьтесь, это у автора такая фишка в цикле статей — наблюдать за отдельно взятой JVM и подсчитывать в ней байтики на синтетических примерах, совершенно в отрыве от контекста и задачи :)
                                    0
                                    Ну тогда покажите мне JVM в которой это не так и я сниму перед Вами шляпу.
                                      +1
                                      Тот же HotSpot JVM, но на 32-битной платформе.
                            +2
                            а какая разница сколько памяти занимают 100 полей в классе? Основные затраты памяти и проц.времени в почти всех жава-приложениях будут на:
                            — ожидание ответа от базы данных
                            — разбор хмл-ов
                            — орм-маппинг
                              0
                              узкое у вас представление о «почти всех жава-приложениях». Вот, например, Мартин Фаулер описывает приложение в котором все хранится в памяти, думаю им приходится задумываться сколько занимают объекты джава.
                                0
                                Я надеюсь это шутка.
                                  0
                                  да уж какие тут шутки
                                +5
                                Простите, долго себя сдерживал, чтобы не комментировать вашу серию «статей» на эту тему, но терпеть больше не могу.

                                Для придания большего драматизма, все расчеты и цифры будут приводится для 64-х разрядной HotSpot JVM.

                                Вообще говоря, ВСЕ что вы пишите имеет (а порой и не имеет) смысл только для HotSpot JVM, одной из реализаций (хоть и самой распространенной) JVM.

                                Денормализация модели

                                Используем смещение в свою пользу

                                Факт выравнивания нигде не специфицируется. Даже в рамках одной JVM. Тот же Hotspot, например, может быть портирован на абстрактную платформу, где вообще нет необходимости в выравнивании полей.
                                Более того, на 32-ной платформе ваш пример абсолютно бесполезен. Я помню, что вы говорите только о 64-ном HotSpot'е, но какой смысл писать 64-bit-only Java-приложение?
                                Следовательно, все ваши выводы бесполезны для большей части Java-приложений.

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

                                Экономия одного поля в классе — это не экономия, а бред. И любой Java-программист, который «экономит на полях» из приведенных вами соображений, IMHO, рано или поздно превратится в говнокодера.

                                Иногда нету никакого смысла использовать byte вместо int, так как за счет выравнивания разница все равно может нивелироваться.

                                А иногда можно сделать четыре byte поля, которые будут занимать ровно 4 байта засчет переупорядочивания полей. Но опять же, считать выравнивание в Java-классах себе дороже…

                                Замена одного поля Integer на int позволит сэкономить 16 байт памяти на объект.

                                Правда, если значение Integer'а в пределах -127-128, то на каждое значение ровно один объект в памяти. А если покрутить ручки HotSpot'а, то диапазон можно сделать еще больше.
                                А если JVM не оракловский? Тогда там вообще все по-другому!
                                И сборщик мусора может даже не заметить пачки аллоцированных примитивных заверток.
                                Кроме этого, есть еще техника Object-инлайна, тогда большая часть оверхедов на эти завертки может нивелироваться.
                                Опять же, Вы делаете какие-то гипотетические выводы, абсолютно не обращая внимания на сами оптимизации в реализациях JVM.

                                Ну и напоследок — не используйте тип Boolean.

                                Про разные use-cases уже написали в комментариях.

                                Но для реальных приложений объем в 4 ГБ не досягаем в виду того, что часть адресного пространства используется для установленных периферийных устройств, например, видео карты.

                                Простите, но это полный бред.
                                Досягаемость 4 Гб зависит ровно от операционной системы и ее настроек. Тот же Windows XP имеет системное ограничение в 2 Гб адресное пространство (3 Гб в более новых версиях при соответствующих настройках). Причина: оптимизация трансляции адресов. В то же время на Linux'ах, если не ошибаюсь, границы уже сломаны и приложения могут использовать все адресное пространство.
                                Ну и конечно же, часть доступного пользовательскому приложению адресного пространства действительно занята системой и системными библиотеками.

                                Поэтому разработчики виртуальных машин решили сэкономить на размере ссылок и ввели опцию -XX:+UseCompressedOops.

                                Просто уточну: разработчики одной виртуальной машины.
                                Будем считать, что эта оговорка была сделана случайно ради «красного словца».

                                Если кому-то кажется, что этот комментарий несет строго негативную оценку, то это правда. Я крайне негативно оцениваю эту статью. Примерно как «белый» SEO-шник оценит «черное» SEO. Или как физик-ученый оценит лже-науку.
                                Я признаю, что многое из написанного действительно справедливо для amd64-x86-compatible windows-version Sun/Oracle HotSpot JVM, но сделанные автором выводы уже поддаются сомнению в отношении того же HotSpot'а на других платформах, а если говорить о других JVM, то эти выводы просто смешны и бесполезны.
                                  +1
                                  Те же мысли были, но писать много лень, в прошлых статьях пытался, да всё впустую :) Предполагаю, что автор всерьёз не оптимизировал огромные приложения на память и прочие ресурсы. О распределении полей не то чтобы в последнюю очередь надо думать, а скажем так вообще никогда. Писал я и на javame очень много, даже там таким заниматься крайне странно. Ну, размер байт-кода ещё бывало приходилось ухитряться пожимать, но какую-то там непонятную совершенно для java «память» — непонятно. Без ориентирования на строго конкретную реализацию (и даже версию) JVM это совершенно бессмысленно, а это означает, что в Java это бесполезно, не тот инструмент это просто, вот и всё.

                                  З.ы. небольшая поправочка:
                                  Досягаемость 4 Гб зависит ровно от операционной системы и ее настроек. Тот же Windows XP имеет системное ограничение в 2 Гб адресное пространство (3 Гб в более новых версиях при соответствующих настройках). Причина: оптимизация трансляции адресов. В то же время на Linux'ах, если не ошибаюсь, границы уже сломаны и приложения могут использовать все адресное пространство.
                                  Не понял про трансляцию. Мы же видимо про 32-битные системы. Но ведь тут «досягаемость» определяется фактически аппаратно, на 32-битной платформе (предположим, intel) приложение не может никак «сломать границы», ну просто физически. Разумеется, линукс или другой тип системы тут непричём. Если про всякие PAE, то они никак не помогают адресовать процессу больше 4гб, конечно же.
                                    +1
                                    приложение не может никак «сломать границы», ну просто физически.

                                    Здесь я имел в виду, что в отличие от Windows под линукс нет дополнительных ограничений на адресное пространство, т.е. могут использоваться все 4 Гб виртуальной памяти. Но вы правы, выразил я эту мысль коряво :)
                                  +2
                                  Да, мне пофиг на размер объекта. Мне важно, чтобы код легче было поддерживать. Поэтому у меня контейнер будет повторять БЛогику, пока я не увижу, что это узкое место и тут нужно и можно пожертвовать качеством кода ради перформанса или памяти. если у меня синглетон висит в памяти и у него 100 полей Boolean, последнее о чем я буду думать это о его размере. Тем более вся оптимизация под хотспот почемуто.
                                    0
                                    «Во втором случае у нас есть тысяча ссылок на двумерные массивы!»

                                    может всё же «1000 ссылок на массивы длины 2»?
                                      0
                                      Спс, подправил.

                                    Only users with full accounts can post comments. Log in, please.