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

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

4. String Pool экономит память на строках 5. Static collections — главный источник утечек 6. NUMA включайте на многопроцессорных серверах. Не включайте на однопроцессорных — ове...

Версии по языкам: English Russian Ukrainian

🟢 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 ГБ без

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

  1. Хранение слишком много в Heap
    // ❌ Кэш без лимита
    static Map<String, Object> cache = new HashMap<>();
    cache.put(key, value);  // Растёт бесконечно → OOM!
    
  2. Создание лишних объектов
    // ❌ Мусор в Heap
    String s = new String("hello");  // Два объекта:
    // 1. Литерал "hello" в String Pool (объект #1)
    // 2. new String() — обёртка поверх литерала (объект #2)
       
    // ✅
    String s = "hello";  // Один объект из пула
    
  3. Утечки через статические коллекции
    // ❌ 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

  1. Избегайте > 32 ГБ без необходимости (Compressed OOPs)
  2. Группируйте поля по размеру для выравнивания
  3. TLAB — аллокация объектов почти бесплатна
  4. String Pool экономит память на строках
  5. Static collections — главный источник утечек
  6. NUMA включайте на многопроцессорных серверах. Не включайте на однопроцессорных — оверхед без пользы.
  7. 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)]]