Difference between stack memory and heap memory in Java

Every Java application running on the JVM relies on two fundamental memory areas: stack memory and heap memory. Understanding how the JVM divides, allocates, and manages these two regions is essential for writing efficient code, preventing memory leaks, and troubleshooting errors like OutOfMemoryError and StackOverflowError. This guide breaks down how each memory type works, their key differences, and practical strategies to optimize Java memory management.

Stack memory

Stack memory in Java is used for static memory allocation and the execution of threads. Each time a method is invoked, the JVM creates a new block (called a stack frame) on top of the thread’s call stack. This frame stores the method’s local primitive variables, object references, and partial results. When the method completes execution, its stack frame is automatically removed, and the memory is reclaimed immediately.

Because stack memory follows a Last-In-First-Out (LIFO) order, it is highly efficient. The JVM does not need garbage collection to manage stack memory—allocation and deallocation happen automatically as methods are called and returned.

Key characteristics of stack memory

  • Thread-safe by design: Each thread in a Java application has its own private stack, so variables stored on the stack cannot be accessed by other threads.
  • Automatic allocation and deallocation: Memory is allocated when a method is called and freed when the method exits. No manual intervention or garbage collection is required.
  • Stores primitives and references: Local primitive types (int, boolean, double, etc.) and references to objects on the heap are stored on the stack.
  • Limited in size: Stack size is typically smaller than heap size and depends on the operating system and JVM configuration. You can adjust it using the -Xss JVM flag.
  • Fast access: Because of LIFO ordering and contiguous memory allocation, stack access is significantly faster than heap access.
  • StackOverflowError: If the stack runs out of space (commonly due to deep or infinite recursion), the JVM throws a java.lang.StackOverflowError.
Understanding stack memory and heap memory in Java

In contrast, heap memory is where all Java objects and class instances reside. The JVM manages heap memory using garbage collection (GC), which automatically identifies and removes objects that are no longer referenced. Choosing an appropriate garbage collector and designing your application with memory efficiency in mind are critical to managing heap memory effectively. Because memory allocations on the heap are dynamic and require GC overhead, heap memory access is generally slower than stack memory access.

Heap memory: a deep dive

Understanding how heap memory works and how it impacts GC is essential to building a high-performance application. Heap memory in general is segmented into two parts:

  • Young generation
  • Old generation

Young generation

This section is further divided into two more spaces:

  • Eden space

    Any new object that is created in an application is first allocated based on the memory availability in the Eden space. When there is a lack of space in this section, it results in a minor GC. Whenever a minor GC is triggered, all the objects that are no longer used or referred to in the application are removed and the ones that are still referenced are moved to the other section of the young generation, which is the survivor space.

  • Survivor space

    Objects are moved to this space after a minor GC. The survivor space is again split in two parts, S1 and S2. At the time of a minor GC, objects that are referenced are switched between S1 and S2 so that one of the sections is always empty. This is done to release the dependency on the Eden space quickly and have a sequential list of least referenced objects. The two survivor spaces’ roles are switched in each minor GC. Whenever a minor GC causes the switch of objects from S1 to S2 or vice versa, if the object survives this cycle, its survival duration is incremented by one.

Old generation

Objects that live for a predefined duration in the young generation are moved to the old generation when the threshold set by the user is hit. For example, when an object is moved to the survivor space, it is expected to undergo multiple cycles of minor GC. Each time a minor GC happens, the object’s survival rate is incremented by one. If the object survives 16 cycles of minor GC and if the tenuring threshold is set to 16, that object is moved into the old generation space automatically. The default value for the tenuring threshold varies for different garbage collectors and can be configured using JVM flags. Objects in the old generation are maintained until their references are modified in any other part of the running application.

When the space allotted for the old generation is full, it results in a major GC. Then, objects that aren’t referenced in the old generation and the young generation are cleaned.

Stack vs heap: key differences

The table below summarizes the core differences between stack memory and heap memory in Java:

Parameter Stack memory Heap memory
Purpose Stores local variables, method calls, and object references for each thread Stores all dynamically created objects and JRE classes at runtime
Access order Last-In-First-Out (LIFO) No specific order; managed by garbage collection
Size Smaller, OS-dependent; configurable with -Xss Larger; configurable with -Xms and -Xmx
Lifetime Exists only while the method is executing Exists as long as the application is running
Speed Faster due to LIFO and contiguous allocation Slower due to dynamic allocation and GC overhead
Thread safety Thread-safe (each thread has its own stack) Not thread-safe (shared across threads; requires synchronization)
Deallocation Automatic when method returns Managed by the garbage collector
Error on overflow java.lang.StackOverflowError java.lang.OutOfMemoryError

How Java allocates memory: a code example

Consider the following Java program to understand how the JVM uses stack and heap memory during execution:

public class MemoryDemo {
    public static void main(String[] args) {
        int count = 5;                          // primitive on stack
        String label = "orders";                // reference on stack, String in heap (string pool)
        Order order = createOrder(count, label); // reference on stack, object on heap
    }

    static Order createOrder(int count, String label) {
        Order newOrder = new Order(count, label); // new stack frame; object allocated on heap
        return newOrder;                          // stack frame removed on return
    }
}

Here is what happens step by step in memory:

  1. When main() is called, the JVM creates a stack frame for it. The primitive variable count (value 5) is stored directly on the stack. The reference variable label is placed on the stack, pointing to the "orders" String object in the heap’s string pool.
  2. When createOrder() is called, a new stack frame is pushed on top of the main() frame. The parameters count and label are copied to this new frame. Inside the method, new Order(count, label) allocates an Order object on the heap, while the reference newOrder sits on the stack.
  3. When createOrder() returns, its stack frame is popped and the memory is freed instantly. The Order object remains on the heap and is now referenced by the order variable in the main() stack frame.
  4. Once main() finishes, all remaining stack memory for the thread is reclaimed. The heap objects become eligible for garbage collection once no live references point to them.

This example illustrates the fundamental rule: primitives and references live on the stack, while objects live on the heap. Understanding this distinction helps you predict memory behavior, avoid leaks, and optimize performance.

Out of memory

If there isn’t enough space to allocate a new object after a major GC, the application crashes with an out-of-memory error. This could happen for various reasons, like:

  • A random application bug
  • Poor application design
  • An under-provisioned resource
  • Inefficient GC configuration

Random application bug-Java A random application bug
Poor application design in Java Poor application design
Under-provisioned resource-Java Under-provisioned resource
Inefficient Garbage Collector configuration in a JVM Inefficient GC configuration

Random application bug

This is the most common problem in the case of memory management. There is no one-size-fits-all approach for solving random application bugs, but they can be avoided by leveraging a test platform and a robust monitoring solution to provide component-level visibility into each application. The most common scenario is a deadlock.

Poor application design

Poor application design is a top-down issue that shows up gradually. A poor design balloons an issue over a period, and it is essential to periodically revisit these scenarios to keep the technical debt low. If not tracked properly, the code-level debt will pile up on top of the poor design. The most common scenario is an expedited code update to push a product live.

Under-provisioned resource

In most cases, this problem occurs when adequate stress testing is not done. An application designed for n users will not scale for 10n users. When there is uncontrolled growth in the number of users, the memory won’t be sufficient and frequent GC pauses could slow down the entire application. The best way to avoid this is to adopt an infrastructure monitoring solution to check the memory usage at the application level on a regular basis.

Inefficient GC configuration

Either inefficient heap size allocation or the incorrect choice of garbage collector can cause ineffective GC configuration. The common scenarios include missing the -Xms and -Xmx configurations before startup.

Based on the scenarios above, it’s evident that most memory management issues start at the application layer. That’s why it is essential to have a memory-driven thought process during the development of an application. A few common approaches are:

  • Limit the scope of references as much as possible in your code. As the stack gets cleaned up for reference faster, most collections undergo a minor GC. This results in fewer GC pauses.
  • Explicitly make an object eligible for GC.
  • Cache what is necessary and avoid recreating objects repeatedly to ensure that the memory gets allocated only on heap memory.
  • Analyze your application requirements and choose the appropriate garbage collector based on that.
  • Using JVM flags is the golden rule. Depending on how the application is used, you may be able to leverage them. You need to monitor and capture various metrics like throughput, latency, and CPU to arrive at the appropriate flag. As all flags work together, you need to check if two or more flags can be set together to understand the usage of each of them.
  • Enable the verbose GC setting (-verbose:gc) to publish the GC logs after every GC. Note that these are cyclic files that don’t consume any of the CPU.
  • Collect GC logs regularly, and set alerts for various thresholds. Also, enable the heap dump setting (–XX:HeapDumpOnOutOfMemory) for out-of-memory scenarios.
  • Keep track of CPU bumps and correlate them with memory issues whenever possible.

An end-to-end Java monitoring tool is crucial for managing the memory used for an application. Since there is no single approach to solving memory problems, you need to connect multiple dots across platforms, from a web request to the application layer. Site24x7 APM Insight provides real-time JVM metrics including heap and non-heap memory usage, garbage collection counts, and CPU utilization to help you pinpoint memory-related performance bottlenecks before they impact your users.

FAQs

1. How does Site24x7 help monitor Java heap memory?

Site24x7 APM Insight tracks Java heap memory usage in real-time, displaying metrics for Eden space, Survivor space, and Old Generation to help you prevent OutOfMemory errors.

Yes, you can set custom thresholds in Site24x7 to receive instant alerts when heap memory usage exceeds a certain percentage, allowing you to take proactive action. Note that Site24x7 also supports dynamic AI-based anomaly detection for memory metrics.

Site24x7 allows you to monitor continuous memory growth trends and garbage collection efficiency, which are key indicators of potential memory leaks in your application.

Stack memory stores method-specific primitive values and object references in a LIFO order and is thread-safe, while heap memory stores all dynamically created objects with global access and relies on garbage collection for deallocation.

When stack memory is exhausted, the JVM throws a java.lang.StackOverflowError. When heap memory is full and garbage collection cannot free enough space, the JVM throws a java.lang.OutOfMemoryError.

Was this article helpful?
Monitor your applications with ease

Identify and eliminate bottlenecks in your application for optimized performance.

Related Articles