Что хранится в Heap
4. Статические переменные классов: с выходом Java 8 статические поля перенесены из PermGen в кучу и хранятся внутри зеркального объекта java.lang.Class. 5. Классы-обертки над пр...
🟢 Junior Level
Heap (Куча) — это центральная область оперативной памяти, выделяемая виртуальной машиной Java (JVM) для хранения всех динамически создаваемых объектов и структур данных приложения. Куча является общей (Shared Memory) для всех потоков: любой объект, созданный в куче, потенциально доступен любому потоку, обладающему ссылкой на него.
Что конкретно размещается в Heap:
- Все экземпляры классов: любые объекты, созданные с помощью оператора
new(new User(),new Order()). - Все массивы: массивы объектов (
String[]) и массивы примитивов (int[],byte[]). В Java любой массив является полноценным объектом и всегда живет в Heap. - Строковый пул (String Constant Pool): строковые литералы и строки, прошедшие интернирование через
.intern(). - Статические переменные классов: с выходом Java 8 статические поля перенесены из PermGen в кучу и хранятся внутри зеркального объекта
java.lang.Class. - Классы-обертки над примитивами: экземпляры
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-коде, а по размеру типов:
double/long(8 байт)int/float(4 байта)short/char(2 байта)byte/boolean(1 байт)- Ссылки на объекты (OOP — 4 или 8 байт)
String Pool и Compact Strings (Java 9+)
- История размещения String Pool:
- До Java 7: String Pool располагался в PermGen (Permanent Generation). Это приводило к частым
java.lang.OutOfMemoryError: PermGen spaceпри динамической генерации строк. - Начиная с Java 7: String Pool перенесен в Java Heap, благодаря чему неиспользуемые интернированные строки своевременно вычищаются обычным Garbage Collector.
- До Java 7: String Pool располагался в PermGen (Permanent Generation). Это приводило к частым
- 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-приложениях.
- До Java 9:
Сжатые указатели: 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 объектов:
- Они аллоцируются не в Eden, а напрямую в специальную непрерывную последовательность регионов в Old Generation.
- Не копируются сборщиком мусора при Young GC (устраняется overhead на перемещение тяжелых массивов).
- Могут вызывать сильную фрагментацию кучи: если нет достаточного количества свободных непрерывных регионов, 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 каверзных вопроса с ответами
- Где хранятся примитивные переменные: в Heap или в Stack?
- Ответ: Локальные примитивные переменные внутри методов хранятся в стековом фрейме текущего потока в Stack. Однако примитивные поля объектов хранятся в куче (Heap) непосредственно внутри тела соответствующего объекта. Элементы массивов примитивов (
byte[],int[]) также всегда лежат в Heap.
- Ответ: Локальные примитивные переменные внутри методов хранятся в стековом фрейме текущего потока в Stack. Однако примитивные поля объектов хранятся в куче (Heap) непосредственно внутри тела соответствующего объекта. Элементы массивов примитивов (
- Где хранятся статические переменные классов?
- Ответ: Начиная с Java 8, статические переменные хранятся в Heap. Они размещаются как поля внутри зеркального объекта
java.lang.Class, который находится в куче. В нативной памяти Metaspace хранятся только метаданные класса (байткод методов, пул констант, аннотации).
- Ответ: Начиная с Java 8, статические переменные хранятся в Heap. Они размещаются как поля внутри зеркального объекта
- В чём разница между Shallow Size и Retained Size объекта?
- Ответ: Shallow Size — это размер оперативной памяти, занимаемый исключительно самим объектом (его заголовок, примитивные поля, ссылки и padding). Retained Size — это суммарный объем памяти, который будет освобожден сборщиком мусора, если удалить данный объект (то есть его собственный shallow size плюс размер всех объектов, которые достижимы исключительно через него).
- Что такое 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).
Связанные темы
- В чём разница между Heap и Stack — фундаментальное сравнение двух областей памяти
- Что хранится в Stack — детальный разбор структуры стекового фрейма
- Что такое утечка памяти в Java — диагностика накопившихся объектов в Heap
- Что такое поколения в GC — Eden, Survivor, Old Gen
- Что такое Metaspace (или PermGen) — разделение кучи и нативной памяти метаданных