Fundamentals 18 min read

Mastering Java Memory: Heap, Off‑Heap, and No‑Heap Explained with Code

This article breaks down Java's three memory regions—Heap, Off‑Heap, and No‑Heap—detailing their definitions, management mechanisms, performance impacts, typical use cases, and provides concrete code samples for allocating each type of memory.

FunTester
FunTester
FunTester
Mastering Java Memory: Heap, Off‑Heap, and No‑Heap Explained with Code

In Java applications, Heap, No‑Heap, and Off‑Heap are three distinct memory areas that play crucial roles in performance and resource management; understanding their differences enables developers to choose the right region for a given workload.

Overview

The three regions can be compared across several attributes:

Definition : Heap stores object instances; No‑Heap holds class metadata, method area, and thread stacks; Off‑Heap is memory outside the JVM managed manually.

Management : Heap is automatically allocated and reclaimed by the GC; No‑Heap is allocated by the JVM but not directly controllable; Off‑Heap requires explicit allocation and release.

GC Impact : Heap is subject to GC pauses; No‑Heap does not participate in GC; Off‑Heap is completely independent of GC.

Typical Content : Heap holds dynamic objects; No‑Heap stores class metadata, static variables, and thread stacks; Off‑Heap is used for large data structures and long‑lived objects such as caches.

Performance : Heap may cause stop‑the‑world (STW) events; No‑Heap offers higher throughput because it avoids GC; Off‑Heap improves concurrency by sidestepping GC entirely.

Allocation Method : Heap objects are created with the new keyword; No‑Heap memory is allocated at JVM startup; Off‑Heap uses ByteBuffer.allocateDirect() or JNI/Unsafe.

Typical Scenarios : Heap for ordinary business logic; No‑Heap for class loading, static data, and thread stacks; Off‑Heap for high‑performance caches, I/O buffers, and big‑data processing.

Heap Basics

Heap is the default memory area where all Java objects and arrays reside. Objects are created with new and automatically managed by the JVM’s garbage collector. The heap is divided into Eden, Survivor, and Old generations, each serving a different lifecycle stage.

Heap size can be tuned with the JVM flags -Xms (initial size) and -Xmx (maximum size) to match application demands.

Common use cases include business‑logic objects, collection elements such as ArrayList, HashMap, and temporary caches.

Automatic memory management eliminates the need for explicit deallocation.

Objects in the heap can be shared across threads, facilitating concurrent processing.

The GC automatically reclaims unreachable objects, reducing memory‑leak risk.

However, heap memory has drawbacks:

Large heaps increase GC frequency, causing performance jitter and STW pauses, especially under high‑load or big‑data workloads.

Full GC can introduce noticeable latency.

Frequent allocation and reclamation may lead to fragmentation.

Off‑Heap (Non‑Heap) Memory

Off‑Heap resides outside the JVM heap and must be manually allocated and freed, typically via DirectByteBuffer, Unsafe, or third‑party libraries like Netty or RocksDB.

Key characteristics:

Manual management mimics C/C++ style allocation.

Not subject to GC, eliminating GC‑induced latency.

Direct access via JNI or NIO reduces data copying and boosts I/O throughput.

Size can be configured with -XX:MaxDirectMemorySize, defaulting to the maximum heap size.

Typical scenarios include large‑scale data processing (e.g., Kafka, Flink), high‑performance caches, and network programming where DirectByteBuffer avoids extra copies.

Advantages:

Reduces GC pressure, ideal for high‑concurrency and big‑data workloads.

Provides a larger addressable memory space beyond the heap limit.

Disadvantages:

Manual management increases complexity and risk of leaks.

Higher development cost compared to heap allocation.

No‑Heap (Metaspace & Thread Stacks)

No‑Heap refers to memory areas outside the Java heap, primarily Metaspace (class metadata) and thread stacks. Since Java 8, Metaspace replaced the PermGen and grows automatically, but its size can be limited with -XX:MaxMetaspaceSize.

Typical uses:

Storing class definitions and method metadata—crucial for large microservice deployments or frameworks that generate many classes (e.g., Hibernate, Spring).

Thread stacks for each Java thread, essential for high‑concurrency servers.

Static variables that need to be shared across the application.

JVM tuning to prevent OutOfMemoryError and improve performance.

Benefits:

Class metadata resides outside the heap, reducing the chance of OOM caused by excessive class loading.

No direct GC impact, so service latency is less affected.

Drawbacks:

Requires careful sizing; misconfiguration can cause Metaspace or stack overflows.

Debugging and monitoring are more complex than heap memory.

Memory Allocation Practice

Heap Allocation

Heap memory is allocated simply by creating objects. The following example creates a large array of strings, each element stored in the heap:

public class HeapMemoryExample {
    public static void main(String[] args) {
        // Create objects allocated on the heap
        String[] largeArray = new String[1000000]; // allocate many objects
        for (int i = 0; i < largeArray.length; i++) {
            largeArray[i] = "String number " + i; // each object lives in heap
        }
        System.out.println("Allocated memory for objects on the heap");
    }
}

Off‑Heap Allocation

Using ByteBuffer.allocateDirect() to reserve 10 MB of off‑heap memory and write sequential bytes:

import java.nio.ByteBuffer;

public class OffHeapMemoryExample {
    public static void main(String[] args) {
        // Allocate 10 MB off‑heap
        ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
        // Write data into off‑heap buffer
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }
        System.out.println("Allocated memory in off‑heap");
    }
}

Tips: ByteBuffer.allocateDirect() creates a 10 MB off‑heap block that is not managed by the GC, suitable for large buffers and high‑performance I/O.

Off‑heap memory must be released manually, e.g., via sun.misc.Cleaner or by invoking Unsafe.freeMemory().

Releasing with Cleaner:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean(); // immediately free off‑heap memory

Using Unsafe (not recommended for production) to allocate and free 1 MB:

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class OffHeapMemoryWithUnsafe {
    private static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Unsafe unsafe = getUnsafe();
        long address = unsafe.allocateMemory(1024 * 1024); // 1 MB
        unsafe.setMemory(address, 1024 * 1024, (byte) 0);
        System.out.println("Unsafe allocated off‑heap memory.");
        unsafe.freeMemory(address);
        System.out.println("Freed unsafe off‑heap memory.");
    }
}

Tip: Direct use of Unsafe gives full control over raw memory but requires meticulous cleanup to avoid leaks.

No‑Heap Allocation

Metaspace can be stressed by dynamically generating many classes with Javassist, causing the JVM to allocate more metadata memory:

import java.util.ArrayList;
import javassist.ClassPool;

public class MetaspaceMemoryExample {
    public static void main(String[] args) {
        // Dynamically create many classes to fill Metaspace
        ClassPool pool = ClassPool.getDefault();
        ArrayList<Class<?>> classes = new ArrayList<>();
        try {
            for (int i = 0; i < 10000; i++) {
                Class<?> c = pool.makeClass("Class" + i).toClass();
                classes.add(c); // load into Metaspace
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Loaded many classes to consume Metaspace.");
    }
}

Tip: Limit Metaspace size with -XX:MaxMetaspaceSize to prevent uncontrolled growth.

Thread stacks consume No‑Heap memory; creating many threads demonstrates this effect:

public class ThreadStackExample {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(10000); // keep thread alive to hold stack memory
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println("Started threads, allocating stack memory.");
    }
}

Tip: Each new thread allocates its own stack; massive thread creation visibly increases No‑Heap consumption.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMperformanceMemory ManagementHeapOff-HeapNo-Heap
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.