🧠 Section 3 · Question #2

What is stored in Heap

The JVM Heap is the primary runtime data area created when the Java Virtual Machine starts up. It is globally shared across all threads (Shared Memory): any object instantiated...


🟢 Junior Level

The JVM Heap is the primary runtime data area created when the Java Virtual Machine starts up. It is globally shared across all threads (Shared Memory): any object instantiated in the heap is accessible by any thread that holds a reference to it.


What Resides in the Heap

  1. All Class Instances: Every object created using the new keyword (e.g., new User(), new HashMap<>()).
  2. All Arrays: Both object arrays (String[]) and primitive arrays (int[], byte[]). In Java, arrays are true heap objects.
  3. The String Constant Pool: String literals and strings explicitly interned via String.intern().
  4. Static Class Variables: Since Java 8, static variables are stored in the Heap as fields of the mirror java.lang.Class object.
  5. Boxed Primitive Wrappers: Instances of Integer, Double, Boolean, whether created explicitly or through autoboxing (Integer.valueOf(42)).

The Classic Junior Trap: “Where Do Primitives Live?”

The storage location of a primitive variable depends strictly on where it is declared:

  • Local Variable in a Method: Stored in the Stack Frame of the executing thread (int count = 10;).
  • Instance Field of a Class: Stored on the Heap within the memory footprint of the enclosing object (class Account { int balance = 100; }).
  • Element of a Primitive Array: Stored on the Heap inside the contiguous memory block of the array object (int[] values = new int[5];).
public class MemoryAllocationDemo {
    private int id = 101; // Heap (instance field of MemoryAllocationDemo)

    public void process() {
        int localCount = 42;            // Stack (local variable of method)
        Point point = new Point(5, 10); // Reference 'point' in Stack -> Point object in Heap
        int[] data = new int[]{1, 2};   // Reference 'data' in Stack -> int[] array in Heap
    }
}

🟡 Middle Level

Java Object Layout (JOL) in 64-Bit HotSpot

Every object allocated on the 64-bit JVM Heap adheres to a structured, 8-byte aligned layout:

┌────────────────────────────────────────────────────────┐
│                   OBJECT HEADER                        │
├────────────────────────────────────────────────────────┤
│ 1. Mark Word (8 bytes)                                 │
│    - Identity HashCode, Age bits (0–15), Lock state   │
├────────────────────────────────────────────────────────┤
│ 2. Klass Word (4 bytes with Compressed Class Pointers) │
│    - Pointer to class metadata in Metaspace            │
├────────────────────────────────────────────────────────┤
│ 3. Array Length (4 bytes, present ONLY in arrays)      │
├────────────────────────────────────────────────────────┤
│ 4. Instance Data (Field values)                        │
│    - Primitive fields and reference pointers           │
├────────────────────────────────────────────────────────┤
│ 5. Padding (Alignment zeroes)                          │
│    - Pads total object footprint to a multiple of 8B   │
└────────────────────────────────────────────────────────┘

Field Packing & Reordering

To prevent memory gaps, the JVM does not place fields in memory in the order they are declared in Java source code. Instead, it reorders them by size:

  1. double / long (8 bytes)
  2. int / float (4 bytes)
  3. short / char (2 bytes)
  4. byte / boolean (1 byte)
  5. Object references (OOPs — 4 or 8 bytes)

Memory Footprint Calculations (Compressed OOPs):

  • new Object(): 8B (Mark Word) + 4B (Klass Word) + 4B (Padding) = 16 bytes.
  • new Integer(42): 8B (Mark Word) + 4B (Klass Word) + 4B (int value) = 16 bytes.
  • new Long(42L): 8B (Mark Word) + 4B (Klass Word) + 8B (long value) + 4B (Padding) = 24 bytes.

String Pool Evolution & Compact Strings (Java 9+)

  1. Relocation of the String Pool:
    • Java 6 and earlier: The String Pool resided in PermGen (Permanent Generation), which had a fixed size and frequently failed with OutOfMemoryError: PermGen space.
    • Java 7+: The String Pool was moved into the Heap, allowing unreferenced interned strings to be reclaimed by standard Garbage Collection.
  2. Compact Strings (Java 9 / JEP 254):
    • Historically, java.lang.String stored characters as a char[], where every character occupied 2 bytes (UTF-16), even for pure ASCII text.
    • Since Java 9, strings use byte[] value paired with a 1-byte encoding flag (byte coder). If all characters fit into Latin-1 (ISO-8859-1), each character occupies only 1 byte, reducing overall enterprise heap consumption by 15–20%.

Compressed OOPs & The 32 GB Limit

On 64-bit JVMs, HotSpot enables Compressed Ordinary Object Pointers (-XX:+UseCompressedOops) by default for heaps under 32 GB:

  • Because objects are aligned to 8-byte boundaries, the lowest 3 bits of any object address are always zero (000).
  • The JVM shifts memory addresses right by 3 bits (address >> 3) to store 64-bit addresses inside 32-bit (4-byte) reference fields.
  • When loading a reference into a CPU register, it shifts left (address << 3).
  • This allows 4-byte references to address up to: \(2^{32} \times 8 \text{ bytes} = 32 \text{ GB}\)

[!WARNING] Setting -Xmx33g disables Compressed OOPs, doubling reference sizes to 8 bytes and consuming 3–4 GB of additional RAM purely for pointers.


🔴 Senior Level

Shallow Size vs Retained Size

When diagnosing memory leaks and analyzing Heap Dumps using tools like Eclipse MAT or JProfiler, understanding object size metrics is critical:

┌────────────────────────────────────────────────────────┐
│  Shallow Size = The memory consumed by the object      │
│  itself (Header + immediate fields + padding)          │
├────────────────────────────────────────────────────────┤
│  Retained Size = Shallow Size                          │
│  + The size of ALL objects reachable EXCLUSIVELY       │
│    through this object (Dominator Tree)                │
└────────────────────────────────────────────────────────┘

Concrete Example: new ArrayList<>(100)

  • Shallow Size: 12 bytes (Header) + 4 bytes (reference to array) + 4 bytes (int size) + 4 bytes (padding) = 24 bytes.
  • Retained Size: Its own 24 bytes + the backing Object[100] array (424 bytes) = 448 bytes. If 100 User objects are placed into this list and referenced nowhere else in the application, their combined heap footprint is added to the list’s Retained Size.

Humongous Objects in the G1 Garbage Collector

The Garbage-First (G1) collector partitions the heap into equal-sized regions (from 1 MB to 32 MB):

  • An object whose size exceeds 50% of a G1 region is classified as a Humongous Object.
  • Specialized Allocation Rules:
    1. Humongous objects bypass the Young Generation (Eden/Survivor) entirely and are allocated directly into a contiguous block of regions in the Old Generation.
    2. They are not moved during standard Young GC evacuation pauses (avoiding expensive memory-copy overhead for large arrays).
    3. Fragmentation Hazard: If the JVM cannot find a contiguous block of unfragmented G1 regions to accommodate a large array, it triggers an emergency Full GC Stop-the-World pause.

Inspecting Object Footprint via JOL (Java Object Layout)

The OpenJDK JOL tool allows inspecting memory layout directly:

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 bytes
    private int status;     // 4 bytes
    private boolean paid;   // 1 byte
    private Object payload; // 4 bytes (compressed OOP)
}

JOL Output on a 64-bit HotSpot 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

Note: The JVM reorders status before id to fill the 4-byte gap immediately following the 12-byte header, achieving 8-byte alignment without padding waste.


Off-Heap Memory: Bypassing the Heap

When managing massive datasets (in-memory caches, messaging queues like Kafka, or database buffers), storing tens of gigabytes in the Heap causes long GC pause times. Applications utilize Off-Heap Memory:

  • java.nio.ByteBuffer.allocateDirect(): Allocates native OS memory via malloc().
  • Project Panama (Foreign Function & Memory API, Java 22+): Arena and MemorySegment provide deterministic, zero-copy native memory allocation without Garbage Collector overhead.

4 Tricky Questions

1. Where are primitive variables stored: in the Heap or in the Stack?

Answer: The location of primitive variables depends strictly on their declaration scope:

  1. Local variables: Primitives declared inside a method (int x = 10;) are stored in the Stack Frame of the calling thread.
  2. Instance fields: Primitive fields declared inside a class (class User { int age = 20; }) are stored on the Heap within the memory layout of the enclosing object instance.
  3. Static fields: Static primitive fields are stored on the Heap within the java.lang.Class mirror object.
  4. Arrays: Elements of primitive arrays (int[] arr = new int[10];) are always stored on the Heap.

2. Where are static class variables stored in modern Java (Java 8+)?

Answer: In Java 8 and later, static variables are stored directly in the Java Heap. They reside as instance fields within the java.lang.Class object associated with the loaded class. Prior to Java 8, static variables were stored in PermGen (Permanent Generation), which frequently caused OutOfMemoryError: PermGen space. In contrast, the modern Metaspace area is stored in native OS memory and holds only class metadata (method bytecodes, constant pool, annotations), not static variable values.


3. What is the difference between an object’s Shallow Size and its Retained Size in memory profiling?

Answer: Shallow Size is the physical memory consumed exclusively by the object itself—including its header, primitive field values, references to other objects (4 or 8 bytes each), and alignment padding. Retained Size is the total heap memory that would be freed if that specific object were garbage collected. It equals the object’s shallow size plus the shallow sizes of all other objects that are exclusively reachable from this object (as computed by the heap Dominator Tree).


4. What are Humongous Objects in the G1 Garbage Collector, and why can they cause premature Full GC pauses?

Answer: In G1 GC, a Humongous Object is any object whose size exceeds 50% of the standard G1 Region size (which ranges from 1 MB to 32 MB). Humongous objects are allocated directly into the Old Generation within a sequence of contiguous regions, bypassing Eden and Survivor spaces. Because they require physically adjacent regions, they can trigger premature Full GC Stop-the-World pauses even when plenty of total free heap exists, if free regions are fragmented across the heap and no single contiguous span of regions is large enough to fit the object.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Java Heap is the shared runtime memory area where all object instances, arrays, the String Constant Pool, and static class fields reside. Every object in the heap has an Object Header consisting of a Mark Word (storing hash code, lock status, and 4-bit GC age) and a Klass Word (pointing to class metadata). To minimize memory waste, HotSpot uses Compressed OOPs below 32 GB, reducing reference pointers to 4 bytes. When analyzing memory leaks, we evaluate Shallow Size (the object’s own footprint) versus Retained Size (the total memory freed if the object and its exclusive dependency tree are collected).”


Key Architectural Takeaways

  • Heap Contents: All instances created via new, all arrays, String Pool, static variables.
  • Primitives Rule: Local primitives $\to$ Stack; class fields and array elements $\to$ Heap.
  • Object Header: Mark Word (8B) + Klass Word (4B/8B) + Array Length (4B for arrays).
  • Minimum Object Size: new Object() occupies exactly 16 bytes.
  • String Pool: Resides in the Heap (since Java 7); uses Compact Strings with Latin-1 encoding (since Java 9).
  • G1 Humongous Objects: Allocated directly into Old Gen if size $> 50\%$ of region size; causes heap fragmentation.

Red Flags & Anti-Patterns

  • 🚩 Claiming “Primitives are always stored on the Stack”: Forgetting that instance fields and array elements reside in the Heap.
  • 🚩 Asserting that the String Pool is in Metaspace: Moved to the Heap in Java 7.
  • 🚩 Assuming an empty new Object() takes 0 bytes: Overlooking the 12-byte header and 4-byte padding (16 bytes total).
  • 🚩 Ignoring the 32 GB boundary: Setting heap size to 33–34 GB, which disables Compressed OOPs and degrades CPU cache utilization.