Что хранится в Heap?
4. String Pool экономит память на строках 5. Static collections — главный источник утечек 6. NUMA включайте на многопроцессорных серверах. Не включайте на однопроцессорных — ове...
🟢 Junior Level
Heap (Куча) — область виртуальной памяти (от десятков МБ до терабайт), выделенная JVM для хранения объектов. Отделена от Stack, потому что объекты живут дольше вызовов методов.
Что именно:
- Всё, что создано через
new→ в Heap - Массивы → в Heap
- Строки → в Heap (String Pool)
- Статические поля → в Heap
Пример:
public class Example {
static int count = 0; // Static field → в Heap
public void method() {
int x = 10; // Примитив → в Stack
String s = "hello"; // Ссылка в Stack, "hello" → в Heap
User user = new User(); // Ссылка в Stack, объект → в Heap
int[] arr = new int[5]; // Ссылка в Stack, массив → в Heap
}
}
Размер Heap:
- Настраивается:
-Xms(начальный),-Xmx(максимальный) - По умолчанию: зависит от системы (обычно 1/4 RAM)
- Ошибка при переполнении:
OutOfMemoryError: Java heap space
🟡 Middle Level
Что хранится в Heap
1. Инстансы объектов:
new User("Ivan") // Объект User в Heap
new ArrayList<>() // Объект ArrayList + внутренние массивы
2. Массивы (даже примитивов):
int[] numbers = new int[1000]; // Массив — объект → в Heap
byte[] data = new byte[1024]; // Даже byte[] — объект в Heap
3. Статические поля (с Java 8):
public class Config {
private static String DB_URL = "jdbc:..."; // В Heap (объект Class)
}
4. String Pool:
String s1 = "hello"; // В String Pool (часть Heap)
String s2 = "hello"; // Та же ссылка (пул экономит память)
Анатомия объекта в памяти
Object в Heap:
┌─────────────────────────┐
│ Header (12-16 байт) │
│ Mark Word (8 байт) │
│ Klass Pointer (4 байта)│
├─────────────────────────┤
│ Instance Data (поля) │
│ int age = 25 (4 байта)│
│ String name (4 байта - ссылка)│
├─────────────────────────┤
│ Padding (до 8 байт) │
└─────────────────────────┘
Mark Word хранит:
- Hash code объекта
- Age для GC (сколько сборок пережил)
- Состояние блокировок
Klass Pointer:
- Указатель на метаданные класса в Metaspace
- 4 байта с Compressed OOPs (< 32 ГБ Heap)
- 8 байт без Compressed OOPs (> 32 ГБ Heap)
Compressed OOPs оптимизация
32-битная JVM: указатели = 4 байта
64-битная JVM: указатели = 8 байт
Compressed OOPs (32 ГБ < Heap):
Указатель = 4 байта (сдвиг × 8)
→ Экономия 50% памяти на ссылках!
→ Больше данных в кэше процессора
Порог: 32 ГБ Heap
2^32 × 8 байт = 32 ГБ
Почему 31 ГБ быстрее 33 ГБ:
- 31 ГБ → Compressed OOPs включены → -50% памяти на ссылки
- 33 ГБ → Compressed OOPs выключены → больше RAM, но медленнее
- Часто 31 ГБ с Compressed OOPs быстрее, чем 33 ГБ без
Типичные ошибки
- Хранение слишком много в Heap
// ❌ Кэш без лимита static Map<String, Object> cache = new HashMap<>(); cache.put(key, value); // Растёт бесконечно → OOM! - Создание лишних объектов
// ❌ Мусор в Heap String s = new String("hello"); // Два объекта: // 1. Литерал "hello" в String Pool (объект #1) // 2. new String() — обёртка поверх литерала (объект #2) // ✅ String s = "hello"; // Один объект из пула - Утечки через статические коллекции
// ❌ Static коллекция растёт бесконечно static List<String> history = new ArrayList<>(); history.add(data); // Никогда не очищается!
🔴 Senior Level
Heap структура (HotSpot JVM)
Heap Layout:
┌──────────────────────────────────────────┐
│ Young Generation │
│ Eden (80%) │
│ Survivor 0 (10%) │
│ Survivor 1 (10%) │
├──────────────────────────────────────────┤
│ Old Generation (Tenured) │
│ Долгоживущие объекты │
├──────────────────────────────────────────┤
│ Humongous Regions (G1) │
│ Объекты > 50% региона │
└──────────────────────────────────────────┘
TLAB (Thread Local Allocation Buffer)
Проблема: сотни потоков аллоцируют одновременно → contention
Решение: каждому потоку свой кусок Eden
Thread 1: TLAB[64KB] → pointer bumping
Thread 2: TLAB[64KB] → pointer bumping
Thread 3: TLAB[64KB] → pointer bumping
Аллокация в TLAB:
obj_ptr = thread.tlab_ptr
thread.tlab_ptr += obj_size
→ 0 синхронизации!
→ Быстрее чем malloc() в C++!
PLAB (Promotion Local Allocation Buffer):
→ Аналог TLAB для Old Gen
→ Используется при копировании выживших объектов
NUMA-Aware Heap
NUMA (Non-Uniform Memory Access):
CPU 1 ── Local Memory (быстрая, 100ns)
CPU 2 ── Remote Memory (медленная, 150ns)
-XX:+UseNUMA:
→ JVM аллоцирует память локально для каждого CPU
→ +10-20% производительности на многопроцессорных серверах
Важно для Highload с > 2 CPU sockets
Object Layout Deep Dive
64-bit JVM, Compressed OOPs:
┌─────────────────────────────────┐
│ Mark Word (8 байт) │
│ Hash: 31 бит │
│ Age: 4 бита (max 15) │
│ Lock: 2 бита │
│ GC: 1 бит │
├─────────────────────────────────┤
│ Klass Pointer (4 байта) │
│ → Сдвиг × 8 → адрес в Metaspace│
├─────────────────────────────────┤
│ Array Length (4 байта) │ ← только для массивов
├─────────────────────────────────┤
│ Instance Data │
│ long (8 байт) │
│ int (4 байта) │
│ short (2 байта) │
│ byte (1 байт) │
│ ref (4 байта с Compressed) │
├─────────────────────────────────┤
│ Padding (до кратности 8) │
└─────────────────────────────────┘
→ Минимальный объект: 16 байт (пустой Object)
→ Integer: 24 байта (16 header + 4 int + 4 padding)
Memory Alignment
Процессор читит память словами (8 байт = 64 бита)
Невыровненный доступ:
[1234][5678][9...]
^ данные跨越 два слова → 2 цикла чтения
Выровненный доступ:
[1234][5678][9...]
^ данные в одном слове → 1 цикл чтения
→ JVM автоматически дополняет до 8 байт
→ Группируйте поля: long/long → int/int → short → byte
Heap Monitoring
// Runtime API
Runtime rt = Runtime.getRuntime();
long max = rt.maxMemory(); // -Xmx
long total = rt.totalMemory(); // Текущий committed
long free = rt.freeMemory(); // Свободно
long used = total - free; // Использовано
// MemoryMXBean
MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = mem.getHeapMemoryUsage();
Future: Project Valhalla
// Value Types (будущее Java):
public final class Point {
public final int x;
public final int y;
}
// Сейчас:
Point[] arr = new Point[100];
→ 100 объектов в Heap (1600 байт заголовков)
→ 100 ссылок (400 байт)
→ Итого: ~4 КБ оверхеда
// С Value Types:
Point[] arr = new Point[100];
→ Данные хранятся прямо в массиве!
→ Без заголовков, без ссылок
→ Кэш-локальность × 10
Production Experience
Реальный сценарий: 33 ГБ Heap медленнее 31 ГБ
- Приложение: Spring Boot,
-Xmx33g - Compressed OOPs отключены → указатели 8 байт
- L3 cache miss rate: 25%
- Решение:
-Xmx30g→ Compressed OOPs включены - Результат: L3 miss rate 12%, +15% throughput
Best Practices
- Избегайте > 32 ГБ без необходимости (Compressed OOPs)
- Группируйте поля по размеру для выравнивания
- TLAB — аллокация объектов почти бесплатна
- String Pool экономит память на строках
- Static collections — главный источник утечек
- NUMA включайте на многопроцессорных серверах. Не включайте на однопроцессорных — оверхед без пользы.
- Object Layout влияет на кэш-локальность
Резюме для Senior
- Heap хранит: объекты, массивы, статические поля, String Pool
- Object Layout = Header (Mark Word + Klass) + Data + Padding
- Compressed OOPs = 32 ГБ порог → 50% экономии на ссылках
- TLAB = lock-free аллокация → быстрее malloc()
- NUMA = локальная память для каждого CPU → +10-20%
- Memory Alignment = кратность 8 байт → 1 цикл чтения
- Value Types (Valhalla) = хранение по значению → без оверхеда
- 31 ГБ часто быстрее 33 ГБ из-за Compressed OOPs
🎯 Шпаргалка для интервью
Обязательно знать:
- В Heap: все объекты (
new), массивы (даже примитивов), статические поля, String Pool - Минимальный объект на 64-битной JVM с Compressed OOPs: 16 байт (header 12 + padding)
- Compressed OOPs сжимают ссылки с 8 до 4 байт при Heap < 32 ГБ
- TLAB — приватный буфер потока в Eden, аллокация = pointer bumping (0 синхронизации)
- String Pool — часть Heap, строки-литералы переиспользуются
- Static коллекции — главный источник утечек памяти
- 31 ГБ Heap часто быстрее 33 ГБ из-за Compressed OOPs
Частые уточняющие вопросы:
- Почему
new String("hello")создаёт 2 объекта? — Литерал “hello” в String Pool + обёрткаnew String() - Что хранит Mark Word? — Hash code, age для GC, состояние блокировок
- Что такое PLAB? — Promotion Local Allocation Buffer, аналог TLAB для Old Gen при копировании выживших объектов
- Почему статические коллекции — частая утечка? — Static field = GC Root, объекты никогда не будут собраны
Красные флаги (НЕ говорить):
- «Примитивы хранятся в Heap» — локальные примитивы хранятся в Stack
- «String Pool находится в Metaspace» — с Java 7 String Pool в Heap
- «Heap чистится автоматически при выходе из метода» — Heap чистит GC, Stack чистится при выходе из метода
Связанные темы:
- [[1. В чём разница между Heap и Stack]]
- [[3. Что хранится в Stack]]
- [[6. Что такое утечка памяти в Java]]
- [[8. Что такое поколения в GC (young, old, metaspace)]]
- [[11. Что такое Metaspace (или PermGen)]]