Java Virtual Machine (JVM) Memory

Introduction

Simply put, Java source code is compiled by the Java compiler into bytecode.

The JVM loads this compiled bytecode and executes it.

This forms the basis of Java’s well-known “write once, run anywhere” capability, enabling platform independence by allowing the same compiled bytecode to be executed on any system equipped with a compliant Java Virtual Machine (JVM).

JVM Runtime Memory Areas

The memory structure of the JVM is divided into FIVE main runtime memory areas.

1. Program Counter (PC) Register

2. Java Virtual Machine Stack

When the required call stack depth exceeds the maximum allowed by the JVM, a StackOverflowError will be thrown.

This typically occurs when a recursive function lacks a proper termination condition. One example as shown below.

public class JVMStackOverflowError {
    public void recursiveFunc() {
        recursiveFunc(); // No termination condition
    }

    public static void main(String[] args) {
        JVMStackOverflowError example = new JVMStackOverflowError();
        example.recursiveFunc(); // Triggers StackOverflowError
    }
}

In the example, the recursiveFunc() method continuously calls itself, resulting in stack frame accumulation until the JVM can no longer allocate more stack memory for the thread. This eventually causes a StackOverflowError.

3. Native Method Stack

The Java Virtual Machine stack is used to store frames. The Java Virtual Machine is also free to use one or more stacks to support native methods.

4. Heap

When new objects are continuously created and all of them remain reachable (i.e., cannot be reclaimed by the garbage collector), the heap may eventually run out of space, resulting in an OutOfMemoryError.

import java.util.LinkedList;
import java.util.List;

public class JVMOutOfMemoryError {
    public static void main(String[] args) {
        List<Object> list = new LinkedList<>();
        while (true) {
            list.add(new Object()); // Keeps consuming heap memory
        }
    }
}

In above code, a LinkedList keeps growing as new Object instances are continuously added to it. Since all added objects are still referenced by the list, they cannot be garbage collected, which eventually leads to a heap overflow and triggers an OutOfMemoryError.

5. Method Area

In Java 8 and later, implemented as Metaspace, which uses native memory instead of heap.

6Plus. Runtime Constant Pool

After introducing the five parts of JVM memory, I would like to highlight the Runtime Constant Pool separately. Although it is technically a part of the Method Area, it plays a critical role during class loading and bytecode execution.

Further Topic: Direct Memory Usage in Java

Direct memory may also be frequently used during program execution. Starting from JDK 1.4, the introduction of the New Input/Output (NIO) package brought a new I/O mechanism based on Channels and Buffers.

This allows Java programs to allocate off-heap memory directly using native libraries, and then operate on it through a DirectByteBuffer object, which resides in the Java heap and serves as a reference to that off-heap memory.

One Example

The ByteBuffer.allocateDirect() method is called repeatedly in a loop to allocate 1MB of direct memory each time. The returned ByteBuffer instances (which are technically DirectByteBuffer objects) are stored in a list without being released, eventually triggering an OutOfMemoryError: Direct buffer memory.

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;

public class DirectMemoryOOMError {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<ByteBuffer> list = new LinkedList<>();
        while (true) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1MB);
            list.add(byteBuffer);
        }
    }
}