DEV Community

Ramanjaneyulu Boya
Ramanjaneyulu Boya

Posted on

Java Memory Management Explained: Heap, Stack, Metaspace and Garbage Collection

Understanding how the JVM allocates memory, manages objects, performs garbage collection and handles memory-related problems in real-world Java applications.

JVM runtime memory architecture showing heap, stack, metaspace and other memory areas

Introduction

Have you ever wondered what happens inside the JVM when you create a Java object?

Consider a simple statement:

Customer customer = new Customer("Ramu");
Enter fullscreen mode Exit fullscreen mode

It looks like a straightforward object creation. However, several things happen behind the scenes:

  • The JVM allocates memory for the object.
  • A reference to the object is maintained in the executing thread's stack frame.
  • The object is stored in the heap.
  • The JVM manages the object's lifecycle.
  • When the object is no longer reachable, it becomes eligible for garbage collection.

Understanding these operations is essential for every Java developer, particularly when building high-performance enterprise applications.

In production environments, improper memory management can lead to:

  • OutOfMemoryError
  • Frequent garbage collection
  • Application latency
  • Increased CPU utilization
  • Memory leaks
  • Application crashes

In this article, we'll explore Java memory management from the JVM perspective, using Java 21 examples and practical enterprise application scenarios.

1. Understanding JVM Memory Architecture

The Java Virtual Machine divides memory into different runtime areas to manage application execution efficiently.

Some memory areas are shared across application threads, while others are created separately for each thread.

The major JVM runtime memory areas include:

  1. Heap
  2. Java Stack
  3. Method Area
  4. Program Counter Register
  5. Native Method Stack

Additionally, JVM implementations use native memory areas such as Metaspace and Code Cache.

JVM Memory Architecture

Java heap memory divided into young generation, Eden, survivor spaces S0 and S1, and old generation

Let's understand each component.

1.1 Heap Memory

Heap is the primary memory area where Java objects and arrays are allocated.

Important characteristics:

  • Shared across application threads.
  • Managed by the garbage collector.
  • Stores objects created using new.
  • Usually the largest JVM memory area.

Example:

public class CustomerService {

    public void createCustomer() {

        Customer customer =
                new Customer("Ramu");

        System.out.println(customer.getName());
    }
}
Enter fullscreen mode Exit fullscreen mode

The Customer object is allocated on the heap, while the local reference variable customer resides in the executing method's stack frame.

1.2 Java Stack Memory

Each thread has its own Java stack.

Whenever a method is invoked, the JVM creates a stack frame.

A stack frame contains:

  • Local variables
  • Operand stack
  • Runtime constant pool reference
  • Method execution information

Example:

public void processPayment() {

    int amount = 5000;

    Payment payment =
            new Payment(amount);

    validatePayment(payment);
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • amount is a primitive local variable.
  • payment is a local reference variable.
  • The actual Payment object is allocated on the heap.

Each method invocation creates a new stack frame, and the frame is removed when the method completes.

1.3 Metaspace

Metaspace stores class metadata required by the JVM.

It was introduced in Java 8, replacing PermGen.

Examples of metadata include:

  • Class structure information
  • Method metadata
  • Runtime constant pool information
  • Field information
  • Method bytecode-related metadata

Unlike heap memory, Metaspace uses native memory.

An important consideration is that excessive class loading or classloader retention can cause Metaspace-related memory problems.

1.4 Program Counter Register

Each JVM thread maintains its own Program Counter (PC) register.

It identifies the JVM instruction currently being executed by that thread.

1.5 Native Method Stack

Native Method Stack supports execution of native methods, typically implemented using languages such as C or C++ through JNI.

2. Understanding Heap Memory Generations

Heap memory is where most Java developers encounter garbage collection concepts.

In a traditional generational heap model, memory is divided into:

  • Young Generation
  • Old Generation

The Young Generation is further divided into:

  • Eden Space
  • Survivor Space S0
  • Survivor Space S1

Java garbage collection workflow showing GC roots, reachable objects, unreachable objects, marking and memory reclamation

2.1 Eden Space

Most new objects are initially allocated in Eden.

Example:

Customer customer =
        new Customer("Ramu");
Enter fullscreen mode Exit fullscreen mode

The object is typically allocated in Eden when the JVM uses a generational collector.

2.2 Survivor Spaces

Survivor spaces are used to hold objects that survive garbage collection in the Young Generation.

The JVM uses two survivor spaces, conventionally called S0 and S1.

Objects surviving a minor collection may move between survivor spaces.

2.3 Old Generation

Objects that survive multiple young-generation collections may eventually be promoted to the Old Generation.

Examples include:

  • Long-lived application configuration objects
  • Cached reference data
  • Shared application-level objects
  • Long-running session-related objects

Promotion depends on collector-specific policies and object age and size; it is not simply a fixed rule that every object follows.

3. How Garbage Collection Works

Garbage Collection (GC) is the JVM's automatic memory management mechanism.

Its primary responsibility is to identify objects that are no longer reachable and reclaim their memory.

An object becomes eligible for garbage collection when it is no longer reachable through the JVM's active reference paths.

For example:

public class MemoryExample {

    public static void main(String[] args) {

        Customer customer =
                new Customer("Ramu");

        customer = null;

    }
}
Enter fullscreen mode Exit fullscreen mode

After assigning null, the object may become eligible for garbage collection, assuming no other references exist.

Remember:

Eligible for garbage collection does not mean immediately garbage collected.

The JVM decides when collection occurs.

3.1 Garbage Collection Workflow

A simplified way to understand garbage collection is through these stages:

  1. Identify GC Roots.
  2. Determine reachable objects.
  3. Identify unreachable objects.
  4. Reclaim eligible memory.
  5. Perform collector-specific compaction or relocation where applicable.

Java garbage collection workflow showing GC roots, reachable objects, unreachable objects, marking and memory reclamation

Common GC Roots include:

  • Active thread stack references
  • Static references
  • JNI references
  • Other JVM-managed references

The actual algorithms differ between garbage collectors.

4. Types of Garbage Collection

Java provides multiple garbage collectors designed for different application requirements.

4.1 Serial Garbage Collector

  • Uses a single thread for garbage collection.
  • Suitable for small applications.
  • Can introduce longer pauses as heap size increases.

4.2 Parallel Garbage Collector

  • Uses multiple threads for garbage collection.
  • Focuses on application throughput.
  • Useful for workloads where throughput is more important than minimizing individual pause times.

4.3 G1 Garbage Collector

G1 (Garbage First) is the default garbage collector for typical server-class configurations in modern Java.

Important characteristics:

  • Divides heap into multiple regions.
  • Supports concurrent marking.
  • Prioritizes regions with more reclaimable memory.
  • Aims to meet configurable pause-time goals.

4.4 Z Garbage Collector (ZGC)

ZGC is designed for low-latency applications.

Important characteristics:

  • Performs much of its work concurrently.
  • Aims to maintain very short GC pauses.
  • Supports large heaps.
  • Generational ZGC is available in Java 21 as an experimental feature.

Comparison

Collector Primary focus
Serial GC Simplicity
Parallel GC Throughput
G1 GC Balanced performance
ZGC Low latency

The right collector depends on workload characteristics, heap size, latency requirements and available resources.

5. Understanding Java Memory-Related Errors

Memory problems are common in enterprise applications, especially when applications process large datasets or handle high concurrent traffic.

5.1 OutOfMemoryError: Java Heap Space

Occurs when the JVM cannot allocate additional objects in the heap.

Common causes:

  • Unbounded collections
  • Excessive object creation
  • Memory leaks
  • Large data processing without pagination

Example:

List<byte[]> data = new ArrayList<>();

while (true) {
    data.add(new byte[1024 * 1024]);
}
Enter fullscreen mode Exit fullscreen mode

This continuously allocates memory until the JVM cannot satisfy another allocation.

5.2 OutOfMemoryError: Metaspace

Can occur when the JVM cannot allocate additional class metadata.

Common causes:

  • Excessive dynamic class loading
  • Classloader leaks
  • Improper application redeployment patterns

5.3 StackOverflowError

Occurs when a thread's stack is exhausted.

A common cause is uncontrolled recursion.

public void process() {
    process();
}
Enter fullscreen mode Exit fullscreen mode

This method continuously invokes itself without a termination condition.

6. Memory Leaks in Java Applications

Java has automatic garbage collection, but that does not mean memory leaks are impossible.

A memory leak happens when objects are no longer needed by the application but remain reachable through references.

Consider this example:

public class CustomerCache {

    private static final List<Customer>
            CACHE = new ArrayList<>();

    public void addCustomer(Customer customer) {
        CACHE.add(customer);
    }
}
Enter fullscreen mode Exit fullscreen mode

If customers are continuously added and never removed, the static collection retains references to them.

The garbage collector cannot reclaim these objects while they remain reachable.

Common causes of memory leaks:

  • Static collections
  • Unbounded caches
  • Improper ThreadLocal usage
  • Event listener references
  • Unclosed resources
  • Classloader retention

In production applications, memory leaks can gradually increase heap utilization and eventually cause service failures.

7. JVM Memory Monitoring and Troubleshooting

Understanding JVM memory is only the first step.

Production engineers must also know how to monitor and troubleshoot memory problems.

Useful JVM tools include:

Tool Purpose
jps List running Java processes
jstat Monitor JVM statistics
jmap Inspect heap and generate heap dumps
jstack Capture thread dumps
jcmd Execute diagnostic commands
Java Flight Recorder Profile JVM activity
Java Mission Control Analyze JVM recordings

Example commands:

jcmd <pid> GC.heap_info
Enter fullscreen mode Exit fullscreen mode
jcmd <pid> GC.class_histogram
Enter fullscreen mode Exit fullscreen mode
jcmd <pid> JFR.start duration=60s filename=recording.jfr
Enter fullscreen mode Exit fullscreen mode

For garbage collection logging in Java 21:

-Xlog:gc*:file=gc.log:time,uptime,level,tags
Enter fullscreen mode Exit fullscreen mode

For heap dump generation when an OutOfMemoryError occurs:

-XX:+HeapDumpOnOutOfMemoryError
Enter fullscreen mode Exit fullscreen mode

These diagnostics help identify memory consumption patterns, allocation behavior and possible leaks.

8. JVM Memory Management in Real-World Banking Applications

Consider a banking application built using Java 21, Spring Boot and microservices.

The application processes:

  • Customer information
  • Account details
  • Payment transactions
  • Transaction history
  • Fraud detection requests

Imagine a payment service processing thousands of concurrent requests.

Each request may create multiple temporary objects:

  • Request DTOs
  • Validation objects
  • Domain objects
  • Database entities
  • Response DTOs

Most temporary objects become eligible for garbage collection after request processing completes.

However, application-level caches, shared collections and long-lived references may retain objects for much longer.

Production considerations

  1. Set appropriate JVM heap limits.
  2. Monitor heap utilization and GC pause times.
  3. Avoid unbounded in-memory collections.
  4. Use pagination for large database queries.
  5. Profile object allocation patterns.
  6. Configure container memory limits carefully.
  7. Monitor application memory and container memory independently.
  8. Capture heap dumps when investigating memory leaks.

For applications running on Kubernetes or OpenShift, remember that the JVM operates within container memory constraints.

For example:

-XX:MaxRAMPercentage=75.0
Enter fullscreen mode Exit fullscreen mode

This allows the JVM to calculate its maximum heap based on the available memory it detects, subject to the JVM and container configuration.

The remaining container memory must also accommodate Metaspace, thread stacks, direct buffers, code cache and other native allocations.

9. JVM Memory Tuning in Java 21

Some useful JVM options include:

JVM option Purpose
-Xms Initial heap size
-Xmx Maximum heap size
-Xss Thread stack size
-XX:+UseG1GC Enable G1 GC
-XX:+UseZGC Enable ZGC
-XX:MaxMetaspaceSize Set Metaspace limit
-XX:MaxRAMPercentage Set maximum heap percentage relative to detected RAM

Example:

java \
-Xms2g \
-Xmx4g \
-XX:+UseG1GC \
-Xlog:gc*:file=gc.log \
-jar banking-service.jar
Enter fullscreen mode Exit fullscreen mode

Important: JVM tuning should be based on actual workload measurements rather than blindly applying configuration values.

10. Best Practices for Java Memory Management

Here are some practical guidelines for enterprise Java applications:

  • Avoid unnecessary object creation in performance-critical paths.
  • Prefer bounded caches over unbounded collections.
  • Use pagination for large database operations.
  • Close resources using try-with-resources.
  • Avoid retaining unnecessary references.
  • Monitor garbage collection regularly.
  • Understand container memory limits.
  • Use heap dumps and profiling tools during troubleshooting.
  • Select garbage collectors based on application requirements.
  • Measure performance before and after tuning.

Conclusion

Java memory management is much more than understanding Heap, Stack and Garbage Collection.

It is about understanding how the JVM allocates objects, manages their lifecycle, identifies unreachable references and reclaims memory efficiently.

For enterprise applications, especially banking and financial systems, effective memory management directly influences application stability, performance, scalability and reliability.

As Java developers and architects, understanding JVM internals helps us build applications that are not only functional but also production-ready.

Keep learning. Keep building. Keep architecting.

— Ram The Architect (RTA)


Further Reading and Resources

Discussion

What garbage collector do you primarily use in your production Java applications — G1 GC, Parallel GC or ZGC?

Have you encountered an OutOfMemoryError or a memory leak in a production environment? Share your experience in the comments.

Top comments (0)