Attic
CollectionsArrays

Memory Layout & Internals

HotSpot JVM object header anatomy, Mark Word, Klass Pointer, compressed OOPs, CPU cache line prefetching, covariant typing, and memory profiling

In Java, arrays are not compiler macros or primitive pointers—they are genuine, heap-allocated object instances synthesized dynamically by the JVM. Understanding their low-level memory footprint and CPU cache interactions explains why arrays remain the bedrock of high-performance Java systems.


HotSpot Object Header Anatomy

While standard Java objects have a 12-byte header on 64-bit JVMs with Compressed OOPs (Ordinary Object Pointers), an array object requires a 16-byte header:

Breakdown of the 16-Byte Header

  1. Mark Word (8 Bytes / 64 Bits): Stores object runtime state: identity hashcode, generational garbage collection age (up to 15 promotions), biased locking status, and thread lock synchronization pointers.
  2. Klass Pointer (4 Bytes with Compressed OOPs / 8 Bytes without): Points to the array class metadata in JVM Metaspace (e.g., [I for int[]).
  3. Array Length (4 Bytes / 32 Bits): Stores the number of elements as a signed 32-bit integer. Because length is physically embedded in the header:
    • arr.length is an instantaneous O(1) field access with zero method invocation overhead.
    • The theoretical limit of an array's size is constrained to 2^31 - 1 (Integer.MAX_VALUE).

Memory Footprint & 8-Byte Alignment

HotSpot JVM aligns all heap object sizes to multiples of 8 bytes. If the total byte count is not divisible by 8, padding bytes (0 to 7) are appended at the end:

Memory = align8(16 + (n * elementSize))

Primitive Array vs Boxed Object Array Footprint

The contrast between primitive arrays and boxed object arrays demonstrates the dramatic cost of Java object overhead:

Array TypeLengthRaw Elements PayloadObject OverheadTotal Memory (Heap)
int[100]100100 * 4 B = 400 B16 B header416 Bytes
long[100]100100 * 8 B = 800 B16 B header816 Bytes
Integer[100]100100 * 4 B refs = 400 B16 B header + 100 * 24 B2,816 Bytes (~7× larger!)
ArrayList<Integer> (100)100100 * 4 B refs = 400 B24 B (List) + 16 B (array) + 100 * 24 B2,840 Bytes

The Autoboxing Penalty: An Integer[100] consumes nearly 700% more memory than an int[100]. Each Integer is a full heap object with its own 12-byte header, 4-byte payload, and 8-byte alignment padding (24 bytes total).


CPU Cache Lines & Hardware Prefetching

Modern CPUs do not fetch single bytes from RAM; they load memory in 64-byte chunks called cache lines into the L1/L2 data caches.

Why Primitive Arrays Beat Collections in Throughput

  1. Spatial Locality: Fetching arr[0] automatically loads arr[1] through arr[15] into the L1 cache.
  2. Hardware Prefetching: When the CPU notices a sequential stride through memory, the hardware prefetcher speculatively loads upcoming cache lines into L2/L1 cache before the loop instruction executes.
  3. Pointer Chasing with Objects: Iterating through Integer[] or LinkedList<Integer> requires dereferencing pointers to arbitrary heap addresses. This stalls the CPU pipeline waiting for main memory (50–100 ns latency per cache miss).

Synthesized Runtime Types & JVM Descriptors

Array classes are generated dynamically by HotSpot at runtime. Their runtime names follow JVM field type signatures:

Java Array DeclarationSynthesized JVM Class NameSuperclassImplemented Interfaces
boolean[][Zjava.lang.ObjectCloneable, Serializable
byte[][Bjava.lang.ObjectCloneable, Serializable
char[][Cjava.lang.ObjectCloneable, Serializable
short[][Sjava.lang.ObjectCloneable, Serializable
int[][Ijava.lang.ObjectCloneable, Serializable
long[][Jjava.lang.ObjectCloneable, Serializable
float[][Fjava.lang.ObjectCloneable, Serializable
double[][Djava.lang.ObjectCloneable, Serializable
String[][Ljava.lang.String;java.lang.ObjectCloneable, Serializable
int[][][[Ijava.lang.ObjectCloneable, Serializable
int[][][][[[Ijava.lang.ObjectCloneable, Serializable
ArrayReflectionDemo.java
int[] nums = {1, 2, 3};
Class<?> clazz = nums.getClass();

System.out.println(clazz.getName());       // [I
System.out.println(clazz.getSuperclass()); // class java.lang.Object

for (Class<?> iface : clazz.getInterfaces()) {
    System.out.println(iface.getName());   // java.lang.Cloneable, java.io.Serializable
}

Array Covariance vs Generic Invariance

Java arrays are covariant: if Sub is a subtype of Super, then Sub[] is considered a subtype of Super[].

This design was introduced in Java 1.0 (before generics existed in Java 5) to allow reusable methods like Arrays.sort(Object[]). However, covariance introduces a runtime type vulnerability:

ArrayStoreExceptionDemo.java
String[] strings = new String[5];
Object[] objects = strings; // Compiles due to array covariance

objects[0] = "Valid String";          // OK
objects[1] = Integer.valueOf(42);     // Compiles, but throws ArrayStoreException at runtime!

Why ArrayStoreException Occurs: Every array in the heap retains its runtime component type in its header metadata. When writing to an object array, the JVM performs an internal type check (checkcast). If the incoming object is incompatible with the array's true heap type, an ArrayStoreException is thrown.

Comparison with Generics

FeatureArrays (T[])Generics (List<T>)
Type RelationshipCovariant (String[] is an Object[])Invariant (List<String> is NOT a List<Object>)
Type VerificationReified (checked at runtime by the JVM)Type Erasure (erased to bounds at compile time)
Safety InvariantRuntime failure (ArrayStoreException)Compile-time check (fails at compile time)

HotSpot JVM Boundary Limits

1. Maximum Array Size (Integer.MAX_VALUE - 8)

Why does new int[Integer.MAX_VALUE] fail even with hundreds of gigabytes of heap?

MaxArrayLimit.java
// Throws: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
int[] max = new int[Integer.MAX_VALUE];

The HotSpot JVM reserves header metadata space inside the memory block. The practical maximum length is typically Integer.MAX_VALUE - 8 (2,147,483,639 elements).

2. Zero-Length Arrays

A zero-length array new int[0] still occupies 16 bytes of heap (the 16-byte object header). When writing APIs that return empty arrays, declare a shared static immutable instance to eliminate garbage collection pressure:

EmptyArrayConstant.java
public class DataService {
    // Avoid allocating new int[0] on every call
    public static final int[] EMPTY_INT_ARRAY = new int[0];

    public int[] fetchItems() {
        if (hasNoData()) {
            return EMPTY_INT_ARRAY; // Zero garbage collection overhead
        }
        return new int[]{1, 2, 3};
    }
}

Interview & Deep-Dive FAQ

On this page