Рубрики
Без рубрики

Измерение размеров объектов в СПМ

Узнайте, как измерить размеры объектов Java с помощью различных инструментов, таких как JOL, агенты Java и утилита командной строки jcmd

Автор оригинала: Ali Dehghani.

1. Обзор

В этом уроке мы увидим, сколько места занимает каждый объект в куче Java.

Во-первых, мы познакомимся с различными метриками для расчета размеров объектов. Затем мы рассмотрим несколько способов измерения размеров экземпляров.

Обычно компоновка памяти областей данных среды выполнения не является частью спецификации JVM и оставляется на усмотрение разработчика|/. Поэтому каждая реализация JVM может иметь различную стратегию размещения объектов и массивов в памяти. Это, в свою очередь, повлияет на размеры экземпляров во время выполнения.

В этом уроке мы сосредоточимся на одной конкретной реализации JVM: JVM HotSpot.

Мы также используем термины JVM и HotSpot JVM взаимозаменяемо на протяжении всего урока.

2. Неглубокие, сохраненные и глубокие размеры объектов

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

При вычислении неглубокого размера объекта мы рассматриваем только сам объект. То есть, если объект имеет ссылки на другие объекты, мы учитываем только размер ссылки на целевые объекты, а не их фактический размер объекта. Например:

Мелкий Размер

Как показано выше, мелкий размер экземпляра Triple – это всего лишь сумма трех ссылок. Мы исключаем фактический размер указанных объектов, а именно A1, B1, и C1, из этого размера.

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

Глубокий Размер

Здесь глубокий размер экземпляра Triple содержит три ссылки плюс фактический размер A1, B1, и C1. Следовательно, глубокие размеры рекурсивны по своей природе.

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

Сохраненный Размер

Сохраненный размер экземпляра Triple включает только A1 и C1 в дополнение к самому экземпляру Triple . С другой стороны, этот сохраненный размер не включает в себя B1, так как экземпляр Pair также имеет ссылку на B1.

Иногда эти дополнительные ссылки косвенно делаются самой JVM. Поэтому вычисление сохраненного размера может быть сложной задачей.

Чтобы лучше понять сохраненный размер, мы должны думать в терминах сборки мусора. Сбор экземпляра Triple делает A1 и C1 недоступными, но B1 все еще доступен через другой объект. В зависимости от ситуации сохраненный размер может находиться в любом месте между мелким и глубоким размером.

3. Зависимость

Для проверки компоновки памяти объектов или массивов в JVM мы будем использовать инструмент Компоновка объектов Java ( JOL ). Поэтому нам нужно будет добавить зависимость jol-core :

 
    org.openjdk.jol 
    jol-core    
    0.10 

4. Простые типы Данных

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

System.out.println(VM.current().details());

Приведенный выше код напечатает простые размеры типов данных следующим образом:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Итак, вот требования к пространству для каждого простого типа данных в JVM:

  • Ссылки на объекты занимают 4 байта
  • логические и байтовые значения потребляют 1 байт
  • короткие и char значения потребляют 2 байта
  • значения int и float потребляют 4 байта
  • длинные и двойные значения потребляют 8 байт

Это верно для 32-разрядных архитектур, а также для 64-разрядных архитектур со сжатыми ссылками.

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

4.1. Несжатые ссылки

Если мы отключим сжатые ссылки через -XX:-UseCompressedOops флаг настройки, то требования к размеру изменятся:

# Objects are 8 bytes aligned.
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Теперь ссылки на объекты будут потреблять 8 байт вместо 4 байт. Остальные типы данных по – прежнему потребляют тот же объем памяти.

Кроме того, JVM HotSpot также не может использовать сжатые ссылки, когда размер кучи превышает 32 ГБ ( если мы не изменим выравнивание объекта ).

Суть в том, что если мы явно отключим сжатые ссылки или размер кучи превысит 32 ГБ, ссылки на объекты потребят 8 байт.

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

5. Сложные Объекты

Чтобы рассчитать размер сложных объектов, давайте рассмотрим типичное отношение профессора к курсу:

public class Course {

    private String name;

    // constructor
}

Каждый Профессор, в дополнение к личным данным, может иметь список курсов ов:

public class Professor {

    private String name;
    private boolean tenured;
    private List courses = new ArrayList<>();
    private int level;
    private LocalDate birthDay;
    private double lastEvaluation;

    // constructor
}

5.1. Малый размер: класс Курса

Небольшой размер экземпляров класса Course должен включать 4-байтовую ссылку на объект (для name field) плюс некоторые накладные расходы на объект. Мы можем проверить это предположение с помощью JOL:

System.out.println(ClassLayout.parseClass(Course.class).toPrintable());

При этом будет выведено следующее:

Course object internals:
 OFFSET  SIZE               TYPE DESCRIPTION               VALUE
      0    12                    (object header)           N/A
     12     4   java.lang.String Course.name               N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Как показано выше, размер объекта составляет 16 байт, включая ссылку на объект размером 4 байта на поле name плюс заголовок объекта.

5.2. Малый размер: класс Профессора

Если мы запустим тот же код для класса Professor :

System.out.println(ClassLayout.parseClass(Professor.class).toPrintable());

Затем JOL выведет потребление памяти для класса Professor следующим образом:

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                     VALUE
      0    12                       (object header)                 N/A
     12     4                   int Professor.level                 N/A
     16     8                double Professor.lastEvaluation        N/A
     24     1               boolean Professor.tenured               N/A
     25     3                       (alignment/padding gap)                  
     28     4      java.lang.String Professor.name                  N/A
     32     4        java.util.List Professor.courses               N/A
     36     4   java.time.LocalDate Professor.birthDay              N/A
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Как мы, вероятно, и ожидали, инкапсулированные поля потребляют 25 байт:

  • Три ссылки на объекты, каждая из которых потребляет 4 байта. Таким образом, всего 12 байт для ссылки на другие объекты
  • Один int , который потребляет 4 байта
  • Один логический , который потребляет 1 байт
  • Один двойной , который потребляет 8 байт

Если добавить 12 байт накладных расходов заголовка объекта плюс 3 байта заполнения выравнивания, то общий размер составит 40 байт.

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

5.3. Мелкий размер: экземпляр

Метод sizeOf() в JOL обеспечивает гораздо более простой способ вычисления мелкого размера экземпляра объекта. Если мы запустим следующий фрагмент:

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println("The shallow size is: " + VM.current().sizeOf(course));

Он напечатает мелкий размер следующим образом:

The shallow size is: 16

5.4. Несжатый размер

Если мы отключим сжатые ссылки или используем более 32 ГБ кучи, размер кучи увеличится:

Professor object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                               VALUE
      0    16                       (object header)                           N/A
     16     8                double Professor.lastEvaluation                  N/A
     24     4                   int Professor.level                           N/A
     28     1               boolean Professor.tenured                         N/A
     29     3                       (alignment/padding gap)                  
     32     8      java.lang.String Professor.name                            N/A
     40     8        java.util.List Professor.courses                         N/A
     48     8   java.time.LocalDate Professor.birthDay                        N/A
Instance size: 56 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

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

5.5. Глубокий Размер

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

String ds = "Data Structures";
Course course = new Course(ds);

Глубокий размер экземпляра Course равен мелкому размеру самого экземпляра Course плюс глубокий размер этого конкретного экземпляра String .

С учетом сказанного, давайте посмотрим, сколько места занимает экземпляр String :

System.out.println(ClassLayout.parseInstance(ds).toPrintable());

Каждый экземпляр String инкапсулирует char[] (подробнее об этом позже) и int хэш-код:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0     4          (object header)                           01 00 00 00 
      4     4          (object header)                           00 00 00 00 
      8     4          (object header)                           da 02 00 f8
     12     4   char[] String.value                              [D, a, t, a,  , S, t, r, u, c, t, u, r, e, s]
     16     4      int String.hash                               0
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Минимальный размер этого экземпляра String составляет 24 байта, включая 4 байта кэшированного хэш-кода, 4 байта ссылки char[] и другие типичные накладные расходы на объекты.

Чтобы увидеть фактический размер char[], мы также можем проанализировать его макет класса:

System.out.println(ClassLayout.parseInstance(ds.toCharArray()).toPrintable());

Макет char[] выглядит следующим образом:

[C object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00
      4     4        (object header)                           00 00 00 00
      8     4        (object header)                           41 00 00 f8 
     12     4        (object header)                           0f 00 00 00
     16    30   char [C.                             N/A
     46     2        (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

Итак, у нас есть 16 байт для Курс например, 24 байта для Строка экземпляр, и, наконец, 48 байт для char[]. В общей сложности, глубокий размер этого Курс экземпляр составляет 88 байт.

С введением компактных строк в Java 9 класс String внутренне использует byte[] для хранения символов:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               
      0     4          (object header)                         
      4     4          (object header)                           
      8     4          (object header)                           
     12     4   byte[] String.value # the byte array                             
     16     4      int String.hash                               
     20     1     byte String.coder # encodig                             
     21     3          (loss due to the next object alignment)

Таким образом, на Java 9+ общий размер экземпляра Course составит 72 байта вместо 88 байт.

5.6. Компоновка графа Объектов

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

Например, мы можем увидеть общую площадь экземпляра Course следующим образом:

System.out.println(GraphLayout.parseInstance(course).toFootprint());

Который печатает следующую сводку:

[email protected] footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1        48        48   [C
         1        16        16   com.baeldung.objectsize.Course
         1        24        24   java.lang.String
         3                  88   (total)

В общей сложности это 88 байт. Метод total Size() возвращает общий размер объекта, который составляет 88 байт:

System.out.println(GraphLayout.parseInstance(course).totalSize());

6. Контрольно-измерительные приборы

Для вычисления неглубокого размера объекта мы также можем использовать пакет Java instrumentation и агенты Java. Во-первых, мы должны создать класс с методом premain() :

public class ObjectSizeCalculator {

    private static Instrumentation instrumentation;

    public static void premain(String args, Instrumentation inst) {
        instrumentation = inst;
    }

    public static long sizeOf(Object o) {
        return instrumentation.getObjectSize(o);
    }
}

Как показано выше, мы будем использовать метод getObjectSize() для поиска мелкого размера объекта. Нам также нужен файл манифеста:

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

Затем, используя этот файл MANIFEST.MF , мы можем создать файл JAR и использовать его в качестве агента Java:

$ jar cmf MANIFEST.MF agent.jar *.class

Наконец, если мы запустим какой-либо код с помощью – javaagent:/path/to/agent.jar аргумент, то мы можем использовать метод sizeOf() :

String ds = "Data Structures";
Course course = new Course(ds);

System.out.println(ObjectSizeCalculator.sizeOf(course));

Это выведет 16 в качестве минимального размера экземпляра Course .

7. Статистика класса

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

$ jcmd  GC.class_stats [output_columns]

Например, мы можем видеть размер каждого экземпляра и количество всех экземпляров Course :

$ jcmd 63984 GC.class_stats InstSize,InstCount,InstBytes | grep Course 
63984:
InstSize InstCount InstBytes ClassName
 16         1        16      com.baeldung.objectsize.Course

Опять же, это означает, что размер каждого экземпляра Course составляет 16 байт.

Чтобы просмотреть статистику класса, мы должны запустить приложение с флагом -XX:+UnlockDiagnosticVMOptions tuning.

8. Отвал Кучи

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

$ jcmd  GC.heap_dump [options] /path/to/dump/file

Например:

$ jcmd 63984 GC.heap_dump -all ~/dump.hpro

Это создаст дамп кучи в указанном месте. Кроме того, с параметром -all все доступные и недоступные объекты будут присутствовать в дампе кучи. Без этой опции JVM выполнит полную сборку перед созданием дампа кучи.

После получения дампа кучи мы можем импортировать его в такие инструменты, как Visual VM:

Как показано выше, сохраненный размер единственного экземпляра Course составляет 24 байта. Как упоминалось ранее, сохраненный размер может находиться в любом месте между мелкими (16 байт) и глубокими размерами (88 байт).

Также стоит упомянуть, что визуальная виртуальная машина была частью дистрибутивов Oracle и Open JDK до Java 9. Однако это уже не так, как в Java 9, и мы должны загрузить визуальную виртуальную машину с ее веб-сайта отдельно.

9. Заключение

В этом уроке мы познакомились с различными метриками для измерения размеров объектов во время выполнения JVM. После этого мы действительно измерили размеры экземпляров с помощью различных инструментов, таких как JOL, агенты Java и утилита командной строки jcmd .

Как обычно, все примеры доступны на GitHub .