🧠 Раздел 3 · Вопрос #2

Что хранится в Heap

4. Статические переменные классов: с выходом Java 8 статические поля перенесены из PermGen в кучу и хранятся внутри зеркального объекта java.lang.Class. 5. Классы-обертки над пр...


🟢 Junior Level

Heap (Куча) — это центральная область оперативной памяти, выделяемая виртуальной машиной Java (JVM) для хранения всех динамически создаваемых объектов и структур данных приложения. Куча является общей (Shared Memory) для всех потоков: любой объект, созданный в куче, потенциально доступен любому потоку, обладающему ссылкой на него.


Что конкретно размещается в Heap:

  1. Все экземпляры классов: любые объекты, созданные с помощью оператора new (new User(), new Order()).
  2. Все массивы: массивы объектов (String[]) и массивы примитивов (int[], byte[]). В Java любой массив является полноценным объектом и всегда живет в Heap.
  3. Строковый пул (String Constant Pool): строковые литералы и строки, прошедшие интернирование через .intern().
  4. Статические переменные классов: с выходом Java 8 статические поля перенесены из PermGen в кучу и хранятся внутри зеркального объекта java.lang.Class.
  5. Классы-обертки над примитивами: экземпляры Integer, Long, Boolean, созданные явно или в процессе автобоксинга (Integer.valueOf(42)).

Главный каверзный вопрос для Junior: «Где хранятся примитивы?»

Ответ зависит строго от места объявления переменной:

  • Локальная переменная метода: хранится в Stack текущего потока (int count = 10;).
  • Поле объекта (Instance Field): хранится в Heap внутри структуры данных самого объекта (class Person { int age = 30; }).
  • Элемент массива примитивов: хранится в Heap в непрерывной области памяти массива (int[] numbers = new int[5];).
public class MemoryAllocationDemo {
    private int id = 100; // Heap (поле экземпляра класса)

    public void process() {
        int localVal = 42;             // Stack (локальный примитив)
        Point point = new Point(1, 2); // Ссылка point в Stack -> объект Point в Heap
        int[] data = new int[]{10, 20}; // Ссылка data в Stack -> массив int[] в Heap
    }
}

🟡 Middle Level

Внутренняя анатомия объекта в памяти (Object Layout)

Каждый объект, размещенный в куче 64-битной JVM HotSpot, имеет строго регламентированную структуру:

┌────────────────────────────────────────────────────────┐
│                   OBJECT HEADER                        │
├────────────────────────────────────────────────────────┤
│ 1. Mark Word (8 байт)                                  │
│    - Identity HashCode, Age bits (GC), Состояние лока  │
├────────────────────────────────────────────────────────┤
│ 2. Klass Word (4 байта при Compressed Class Pointers)  │
│    - Ссылка на метаданные класса в Metaspace           │
├────────────────────────────────────────────────────────┤
│ 3. Array Length (4 байта, присутствует только у array) │
├────────────────────────────────────────────────────────┤
│ 4. Instance Data (значения полей объекта)              │
│    - Примитивные типы и ссылки на другие объекты       │
├────────────────────────────────────────────────────────┤
│ 5. Padding (Выравнивание)                              │
│    - Дополнение нулями до кратности 8 байтам           │
└────────────────────────────────────────────────────────┘

Правило переупорядочивания полей (Field Packing)

Для минимизации фрагментации и оверхеда выравнивания JVM переупорядочивает поля объекта не в порядке их объявления в Java-коде, а по размеру типов:

  1. double / long (8 байт)
  2. int / float (4 байта)
  3. short / char (2 байта)
  4. byte / boolean (1 байт)
  5. Ссылки на объекты (OOP — 4 или 8 байт)

String Pool и Compact Strings (Java 9+)

  1. История размещения String Pool:
    • До Java 7: String Pool располагался в PermGen (Permanent Generation). Это приводило к частым java.lang.OutOfMemoryError: PermGen space при динамической генерации строк.
    • Начиная с Java 7: String Pool перенесен в Java Heap, благодаря чему неиспользуемые интернированные строки своевременно вычищаются обычным Garbage Collector.
  2. Compact Strings (JEP 254 в Java 9):
    • До Java 9: java.lang.String внутри содержал массив char[] value, где каждый символ занимал 2 байта (UTF-16), даже если строка содержала только ASCII-символы.
    • С Java 9: строки перешли на byte[] value и 1-байтный флаг кодировки byte coder. Если строка содержит только символы Latin-1 (ISO-8859-1), каждый символ занимает 1 байт, что экономит до 15–20% памяти кучи в enterprise-приложениях.

Сжатые указатели: Compressed OOPs

На 64-битных JVM адрес указателя на объект должен занимать 8 байт. Однако при объеме кучи менее 32 ГБ HotSpot по умолчанию включает флаг -XX:+UseCompressedOops:

  • Так как объекты выравниваются с шагом в 8 байт, младшие 3 бита адреса всегда нулевые.
  • JVM отбрасывает 3 нуля, сжимая ссылку до 4 байт.
  • $2^{32} \times 8 \text{ байт} = 32 \text{ ГБ}$ — предельный объем адресуемой памяти со сжатыми указателями.

💡 Эффект порога 32 ГБ: Увеличение -Xmx с 31 ГБ до 33 ГБ отключает Compressed OOPs. Все ссылки увеличиваются с 4 до 8 байт, что съедает около 3–4 ГБ памяти на одни только указатели и снижает плотность попадания в кэш процессора L1/L2/L3.


🔴 Senior Level

Shallow Size vs Retained Size

При профилировании утечек памяти и анализе Heap Dump (через Eclipse MAT или JProfiler) ключевое значение имеет различие двух метрик размера объектов:

┌────────────────────────────────────────────────────────┐
│  Shallow Size = Собственная память объекта             │
│  (Header + собственные поля + padding)                 │
├────────────────────────────────────────────────────────┤
│  Retained Size = Shallow Size                          │
│  + Память ВСЕХ объектов, достижимых ИСКЛЮЧИТЕЛЬНО      │
│    через данный объект (Dominator Tree)                │
└────────────────────────────────────────────────────────┘

Пример: Пустой список new ArrayList<>(100):

  • Shallow Size: заголовок (12 байт) + ссылка на внутренний массив (4 байта) + int size (4 байта) + padding (4 байта) = 24 байта.
  • Retained Size: собственные 24 байта + размер внутреннего массива Object[100] (424 байта) = 448 байт. Если в список добавить 100 объектов User, которые больше никем в приложении не используются, их вес также войдет в Retained Size этого списка.

Большие объекты: Humongous Objects в сборщике G1

В garbage collector G1 (используемом по умолчанию) куча разбита на тысячи одинаковых регионов (от 1 МБ до 32 МБ в зависимости от размера кучи).

  • Если размер объекта превышает 50% от размера региона G1, он классифицируется как Humongous Object.
  • Особенности размещения Humongous объектов:
    1. Они аллоцируются не в Eden, а напрямую в специальную непрерывную последовательность регионов в Old Generation.
    2. Не копируются сборщиком мусора при Young GC (устраняется overhead на перемещение тяжелых массивов).
    3. Могут вызывать сильную фрагментацию кучи: если нет достаточного количества свободных непрерывных регионов, JVM инициирует досрочную глобальную сборку мусора Full GC.

Анализ распределения памяти через JOL (Java Object Layout)

Инструмент org.openjdk.jol:jol-core позволяет исследовать реальную раскладку байтов в памяти:

import org.openjdk.jol.info.ClassLayout;

public class JolInspection {
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Order.class).toPrintable());
    }
}

class Order {
    private long id;       // 8 байт
    private int status;    // 4 байта
    private boolean paid;  // 1 байт
    private Object payload;// 4 байта (сжатая ссылка)
}

Вывод JOL на 64-битной JVM:

OFFSET  SIZE     TYPE DESCRIPTION                    VALUE
     0     8          (object header: mark word)     ...
     8     4          (object header: class word)    ...
    12     4      int Order.status                   0
    16     8     long Order.id                       0
    24     4   Object Order.payload                  null
    28     1  boolean Order.paid                     false
    29     3          (alignment/padding gap)
Instance size: 32 bytes

Обратите внимание: JVM переставила поле status вперед поля id, чтобы устранить 4-байтный зазор после заголовка (12 байт + 4 байта int = выравнивание по границе 8 байт).


Off-Heap память: когда кучи недостаточно

При работе с десятками гигабайт кэша (in-memory data grids, брокеры сообщений Kafka, Cassandra) аллокация в Heap приводит к долгим паузам GC. Для обхода этой проблемы применяют Off-Heap память (вне кучи):

  • java.nio.ByteBuffer.allocateDirect(): память выделяется ОС напрямую через системный malloc().
  • Project Panama (Foreign Function & Memory API, Java 22+): интерфейсы Arena и MemorySegment обеспечивают безопасную, детерминированную работу с нативной памятью без оверхеда GC.

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«В Heap хранятся все динамические данные приложения: экземпляры классов, созданные через new, массивы любого типа (включая примитивные), строковый пул (String Pool) и статические поля классов (внутри объектов Class с Java 8). Анатомия любого объекта в куче состоит из заголовка (Mark Word + Klass Word), полей данных и байтов выравнивания (Padding). При объеме кучи до 32 ГБ JVM включает Compressed OOPs, сжимая ссылки до 4 байт. При диагностике утечек критично различать Shallow Size (собственный вес объекта) и Retained Size (вес объекта со всем деревом зависимостей, которое освободит GC)».


3–4 каверзных вопроса с ответами

  1. Где хранятся примитивные переменные: в Heap или в Stack?
    • Ответ: Локальные примитивные переменные внутри методов хранятся в стековом фрейме текущего потока в Stack. Однако примитивные поля объектов хранятся в куче (Heap) непосредственно внутри тела соответствующего объекта. Элементы массивов примитивов (byte[], int[]) также всегда лежат в Heap.
  2. Где хранятся статические переменные классов?
    • Ответ: Начиная с Java 8, статические переменные хранятся в Heap. Они размещаются как поля внутри зеркального объекта java.lang.Class, который находится в куче. В нативной памяти Metaspace хранятся только метаданные класса (байткод методов, пул констант, аннотации).
  3. В чём разница между Shallow Size и Retained Size объекта?
    • Ответ: Shallow Size — это размер оперативной памяти, занимаемый исключительно самим объектом (его заголовок, примитивные поля, ссылки и padding). Retained Size — это суммарный объем памяти, который будет освобожден сборщиком мусора, если удалить данный объект (то есть его собственный shallow size плюс размер всех объектов, которые достижимы исключительно через него).
  4. Что такое Humongous объекты в сборщике мусора G1 и почему они опасны?
    • Ответ: В G1 объект считается Humongous, если его размер превышает 50% от размера региона G1. Такие объекты аллоцируются напрямую в специальный непрерывный набор регионов в Old Generation, минуя Eden и Survivor. Они опасны тем, что вызывают фрагментацию кучи: если для нового большого массива не найдется непрерывной последовательности свободных регионов, JVM инициирует принудительный Full GC со Stop-The-World паузой.

Типичные ошибки и Red Flags

  • 🚩 Утверждение: «Все примитивы всегда хранятся только в Stack» (классическая ошибка: примитивные поля классов живут в Heap).
  • 🚩 Заблуждение: «String Pool находится в PermGen / Metaspace» (String Pool был перенесен в Heap еще в Java 7).
  • 🚩 Непонимание разницы между Shallow и Retained Size при разборе утечек памяти.
  • 🚩 Игнорирование порога 32 ГБ кучи (установка -Xmx33g приводит к отключению Compressed OOPs и падению эффективности кэша CPU).

Связанные темы