Question 2 · Section 3

What is stored in Heap?

4. String Pool saves memory on strings 5. Static collections — main source of leaks 6. NUMA — enable on multi-processor servers. Do not enable on single-processor — overhead wit...

Language versions: English Russian Ukrainian

Junior Level

Heap — a virtual memory area (from tens of MB to terabytes) allocated by the JVM for storing objects. Separated from Stack because objects outlive method calls.

What exactly:

  • Everything created via new → in Heap
  • Arrays → in Heap
  • Strings → in Heap (String Pool)
  • Static fields → in Heap

Example:

public class Example {
    static int count = 0;        // Static field → in Heap

    public void method() {
        int x = 10;              // Primitive → in Stack
        String s = "hello";      // Reference in Stack, "hello" → in Heap
        User user = new User();  // Reference in Stack, object → in Heap
        int[] arr = new int[5];  // Reference in Stack, array → in Heap
    }
}

Heap Size:

  • Configurable: -Xms (initial), -Xmx (maximum)
  • Default: depends on system (usually 1/4 of RAM)
  • Overflow error: OutOfMemoryError: Java heap space

Middle Level

What is stored in Heap

1. Object instances:

new User("Ivan")        // User object in Heap
new ArrayList<>()       // ArrayList object + internal arrays

2. Arrays (even of primitives):

int[] numbers = new int[1000];  // Array is an object → in Heap
byte[] data = new byte[1024];   // Even byte[] is an object in Heap

3. Static fields (since Java 8):

public class Config {
    private static String DB_URL = "jdbc:...";  // In Heap (Class object)
}

4. String Pool:

String s1 = "hello";  // In String Pool (part of Heap)
String s2 = "hello";  // Same reference (pool saves memory)

Object Anatomy in Memory

Object in Heap:
┌─────────────────────────┐
│ Header (12-16 bytes)    │
│   Mark Word (8 bytes)   │
│   Klass Pointer (4 bytes)│
├─────────────────────────┤
│ Instance Data (fields)  │
│   int age = 25  (4 bytes)│
│   String name   (4 bytes - reference)│
├─────────────────────────┤
│ Padding (to 8 bytes)    │
└─────────────────────────┘

Mark Word stores:

  • Object hash code
  • Age for GC (how many collections survived)
  • Lock state

Klass Pointer:

  • Pointer to class metadata in Metaspace
  • 4 bytes with Compressed OOPs (< 32 GB Heap)
  • 8 bytes without Compressed OOPs (> 32 GB Heap)

Compressed OOPs Optimization

32-bit JVM: pointers = 4 bytes
64-bit JVM: pointers = 8 bytes

Compressed OOPs (Heap < 32 GB):
  Pointer = 4 bytes (shift × 8)
  → 50% memory savings on references!
  → More data in CPU cache

Threshold: 32 GB Heap
  2^32 × 8 bytes = 32 GB

Why 31 GB is faster than 33 GB:

  • 31 GB → Compressed OOPs enabled → -50% memory on references
  • 33 GB → Compressed OOPs disabled → more RAM, but slower
  • Often 31 GB with Compressed OOPs is faster than 33 GB without

Common mistakes

  1. Storing too much in Heap
    // ❌ Unbounded cache
    static Map<String, Object> cache = new HashMap<>();
    cache.put(key, value);  // Grows infinitely → OOM!
    
  2. Creating unnecessary objects
    // ❌ Garbage in Heap
    String s = new String("hello");  // Two objects:
    // 1. Literal "hello" in String Pool (object #1)
    // 2. new String() — wrapper over literal (object #2)
    
    // ✅
    String s = "hello";  // One object from pool
    
  3. Leaks through static collections
    // ❌ Static collection grows infinitely
    static List<String> history = new ArrayList<>();
    history.add(data);  // Never cleaned!
    

Senior Level

Heap Structure (HotSpot JVM)

Heap Layout:
┌──────────────────────────────────────────┐
│ Young Generation                         │
│   Eden      (80%)                        │
│   Survivor 0 (10%)                       │
│   Survivor 1 (10%)                       │
├──────────────────────────────────────────┤
│ Old Generation (Tenured)                 │
│   Long-lived objects                     │
├──────────────────────────────────────────┤
│ Humongous Regions (G1)                   │
│   Objects > 50% of region                │
└──────────────────────────────────────────┘

TLAB (Thread Local Allocation Buffer)

Problem: hundreds of threads allocating simultaneously → contention

Solution: each thread gets its own piece of Eden
  Thread 1: TLAB[64KB] → pointer bumping
  Thread 2: TLAB[64KB] → pointer bumping
  Thread 3: TLAB[64KB] → pointer bumping

Allocation in TLAB:
  obj_ptr = thread.tlab_ptr
  thread.tlab_ptr += obj_size
  → 0 synchronization!
  → Faster than malloc() in C++!

PLAB (Promotion Local Allocation Buffer):
  → Analog of TLAB for Old Gen
  → Used when copying surviving objects

NUMA-Aware Heap

NUMA (Non-Uniform Memory Access):
  CPU 1 ── Local Memory (fast, 100ns)
  CPU 2 ── Remote Memory (slow, 150ns)

-XX:+UseNUMA:
  → JVM allocates memory locally for each CPU
  → +10-20% performance on multi-processor servers

Important for Highload with > 2 CPU sockets

Object Layout Deep Dive

64-bit JVM, Compressed OOPs:

┌─────────────────────────────────┐
│ Mark Word       (8 bytes)       │
│   Hash: 31 bits                 │
│   Age: 4 bits (max 15)          │
│   Lock: 2 bits                  │
│   GC: 1 bit                     │
├─────────────────────────────────┤
│ Klass Pointer   (4 bytes)       │
│   → Shift × 8 → address in Metaspace│
├─────────────────────────────────┤
│ Array Length    (4 bytes)       │ ← only for arrays
├─────────────────────────────────┤
│ Instance Data                   │
│   long  (8 bytes)               │
│   int   (4 bytes)               │
│   short (2 bytes)               │
│   byte  (1 byte)                │
│   ref   (4 bytes with Compressed)│
├─────────────────────────────────┤
│ Padding (to multiple of 8)      │
└─────────────────────────────────┘

→ Minimum object: 16 bytes (empty Object)
→ Integer: 24 bytes (16 header + 4 int + 4 padding)

Memory Alignment

CPU reads memory in words (8 bytes = 64 bits)

Unaligned access:
  [1234][5678][9...]
   ^  data spans two words → 2 read cycles

Aligned access:
  [1234][5678][9...]
      ^  data in one word → 1 read cycle

→ JVM automatically pads to 8 bytes
→ Group fields by size: long/long → int/int → short → byte

Heap Monitoring

// Runtime API
Runtime rt = Runtime.getRuntime();
long max = rt.maxMemory();         // -Xmx
long total = rt.totalMemory();     // Current committed
long free = rt.freeMemory();       // Free
long used = total - free;          // Used

// MemoryMXBean
MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = mem.getHeapMemoryUsage();

Future: Project Valhalla

// Value Types (future Java):
public final class Point {
    public final int x;
    public final int y;
}

// Now:
Point[] arr = new Point[100];
 100 objects in Heap (1600 bytes of headers)
 100 references (400 bytes)
 Total: ~4 KB overhead

// With Value Types:
Point[] arr = new Point[100];
 Data stored directly in the array!
 No headers, no references
 Cache locality × 10

Production Experience

Real scenario: 33 GB Heap slower than 31 GB

  • Application: Spring Boot, -Xmx33g
  • Compressed OOPs disabled → 8 byte pointers
  • L3 cache miss rate: 25%
  • Solution: -Xmx30g → Compressed OOPs enabled
  • Result: L3 miss rate 12%, +15% throughput

Best Practices

  1. Avoid > 32 GB without need (Compressed OOPs)
  2. Group fields by size for alignment
  3. TLAB — object allocation is almost free
  4. String Pool saves memory on strings
  5. Static collections — main source of leaks
  6. NUMA — enable on multi-processor servers. Do not enable on single-processor — overhead without benefit.
  7. Object Layout affects cache locality

Senior Summary

  • Heap stores: objects, arrays, static fields, String Pool
  • Object Layout = Header (Mark Word + Klass) + Data + Padding
  • Compressed OOPs = 32 GB threshold → 50% savings on references
  • TLAB = lock-free allocation → faster than malloc()
  • NUMA = local memory for each CPU → +10-20%
  • Memory Alignment = multiple of 8 bytes → 1 read cycle
  • Value Types (Valhalla) = store by value → no overhead
  • 31 GB is often faster than 33 GB due to Compressed OOPs

Interview Cheat Sheet

Must know:

  • In Heap: all objects (new), arrays (even primitives), static fields, String Pool
  • Minimum object on 64-bit JVM with Compressed OOPs: 16 bytes (header 12 + padding)
  • Compressed OOPs compress references from 8 to 4 bytes when Heap < 32 GB
  • TLAB — thread-private buffer in Eden, allocation = pointer bumping (0 synchronization)
  • String Pool — part of Heap, string literals are reused
  • Static collections — main source of memory leaks
  • 31 GB Heap is often faster than 33 GB due to Compressed OOPs

Common follow-up questions:

  • Why does new String("hello") create 2 objects? — Literal “hello” in String Pool + new String() wrapper
  • What does Mark Word store? — Hash code, age for GC, lock state
  • What is PLAB? — Promotion Local Allocation Buffer, analog of TLAB for Old Gen when copying surviving objects
  • Why are static collections a common leak? — Static field = GC Root, objects are never collected

Red flags (DO NOT say):

  • “Primitives are stored in Heap” — local primitives are stored in Stack
  • “String Pool is in Metaspace” — since Java 7, String Pool is in Heap
  • “Heap is cleaned automatically on method return” — Heap is cleaned by GC, Stack is cleaned on method return

Related topics:

  • [[1. What is the difference between Heap and Stack]]
  • [[3. What is stored in Stack]]
  • [[6. What is a memory leak in Java]]
  • [[8. What are generations in GC]]
  • [[11. What is Metaspace (or PermGen)]]