Java Under the Microscope: Stack, Heap, and GC in Sample Code

Diagram - Java Memory Model - Heap / Non-Heap / Stack

Heap (memory for objects)
Creates objects using new.
Young Generation: Eden + Survivor.
Old Generation: Objects that have survived multiple GC collections.
The Heap size is usually larger than the Non-Heap size.
Young Generation
Eden
Survivor
Old Generation
Non-Heap
Memory not directly associated with user objects.
Includes Metaspace, Code Cache, Compressed Class Space, and PermGen (for older JVMs).
Metaspace
Static Area
Code Cache
Compressed Class Space
Stack (threads)
Local variables, method parameters, return address.
Each JVM thread has its own stack.
References to objects in the Heap keep them alive.
Thread-1
Thread-2
Thread-3

Java Memory Deep Dive: How Objects, Primitives, and Methods Live in Memory


// The MemoryExample class is stored in the Metaspace
public class MemoryExample {

    // Static variable - stored in the Metaspace / Static area
    static int staticVar = 10;
    // Metaspace:
    // MemoryExample.staticVar = 10

    public static void main(String[] args) {

        // Stack main:
        // args -> reference to an array of strings
        System.out.println("Start of main");

        // Create a Point object on the heap (Eden), the reference p is stored on the stack main
        Point p = new Point(5, 7);
        // Heap (Eden):
        // [Point object header | x=5 | y=7]
        // Main stack: p -> Point on the heap

        // Primitive variable a is stored on the main stack
        int a = 20;
        // Main stack: a = 20

        // Call the multiplyPoint method, passing a reference to p and the primitive a
        // The JVM creates a new stack frame for multiplyPoint
        int result = multiplyPoint(p, a);

        // ---------------- MultiplyPoint frame ----------------
        // pt -> reference to a Point object on the heap
        // factor = copy of a (20)
        // Operand stack for calculations
        // Execution: return pt.x * pt.y * factor = 5 * 7 * 20 = 700
        // ----------------------------------------------------
        // After returning from multiplyPoint, the frame is automatically deleted
        // The main stack is now active again

        System.out.println("Result: " + result);
        // Main stack: print 700

        // Nullify the p reference
        p = null;

        // Heap: Point object {x=5, y=7} is now unreachable
        // The JVM may delete the object automatically when it decides to free memory
        // If the object has survived several GC cycles → Survivor → Old Generation (presumably)

        // Create a new Point object
        Point q = new Point(10, 20);
        // Heap (Eden):
        // [Point object header | x=10 | y=20]
        // Main stack: q -> Point on the heap

        System.out.println("End of main");
        // After main completes:
        // Local variables args, result, q are cleaned up
        // Unreachable objects on the heap will be collected automatically by the GC
    }

    static int multiplyPoint(Point pt, int factor) {
        // ---------------- Stack frame of multiplyPoint ----------------
        // pt -> reference to the Point object on the heap
        // factor = copy of int
        // The JVM operand stack executes pt.x * pt.y * factor
        // ----------------------------------------------------------
        return pt.x * pt.y * factor;
    }
}

// The Point class is stored in Metaspace
class Point {

    int x; // object field is on the heap
    int y; // object field is on the heap

    Point(int x, int y) {
        // Constructor stack frame:
        // parameters x, y are local copies
        this.x = x; // assign the object field on the heap
        this.y = y; // assign the object field on the heap
        // After the constructor completes, the frame is deleted, the object remains on the heap
    }

    @Override
    public String toString() {
        return "Point{" + x + "," + y + "}";
    }
}

Java Memory Example: Explanation

This example shows how the JVM distributes objects and variables between the stack, heap, and static memory, and how objects become candidates for garbage collection (GC).
Key Points
Element Stored Comment
Method local variables (int a, object references) Stack Frame Live only within the method. After exiting the method, the frame is deleted.
Objects (new Point(...)) Heap (heap, Young Generation → Survivor → Old) Created in Eden, can move to Survivor and then to Old if they survive several GC collections.
Static variables (staticVar) Metaspace / Static area They last the entire life of the program and are not touched by the GC.
Unreferenced objects (e.g., after p = null) Heap (in Eden/Survivor/Old) Unreachable objects become candidates for GC and will be removed automatically by the JVM.
Example stack frame
The multiplyPoint(pt, factor) method creates a stack frame that contains:
  • A reference pt to a Point object on the heap
  • A local variable factor
  • The JVM operand stack for calculations
After return, the frame is removed, and the result is returned to the calling method.

Java Caching and GC: How SoftReference Affects the Lifespan of Objects in Memory


import java.lang.ref.SoftReference;
import java.util.HashMap;

public class CacheGCExample {

    // Static cache map — lives in the Metaspace/Static area
    static HashMap
  
   > cache = new HashMap<>();

    public static void main(String[] args) {

        System.out.println("=== Start of main ===");

        // Create a regular object, the reference is stored on the stack
        Point p1 = new Point(100, 200);
        // Heap (Eden): Point object {x=100, y=200}
        // Stack main: p1 -> Point object on the heap

        // Add the object to the cache via SoftReference
        cache.put("first", new SoftReference<>(p1));
        // Heap: SoftReference object {ref -> Point (100,200)}
        // The cache is stored in the Static area
        // Even if p1 becomes null, the object can live as long as the SoftReference holds the reference

        // Remove the direct reference
        p1 = null;
        // Heap: Point object (100,200) is still reachable via SoftReference
        // Stack main: p1 reference removed

        // Call the method that creates temporary objects
        createTemporaryPoints();
        // Inside the multiplyPoint method, the stack frame: temporary objects are deleted after exiting
        // Heap: unreferenced objects, candidate for GC

        // Force garbage collection (for demonstration purposes only)
        System.gc();
        System.out.println("=== After System.gc() ===");

        // Check the cache
        SoftReference
   
     ref = cache.get("first");
        Point cachedPoint = ref.get(); // may return null if the GC has collected the object
        System.out.println("Cached point: " + cachedPoint);

        System.out.println("=== End of main ===");
    }

    static void createTemporaryPoints() {
        // Stack createTemporaryPoints: local references temp1, temp2
        Point temp1 = new Point(1, 2); // Heap: object {x=1, y=2}
        Point temp2 = new Point(3, 4); // Heap: object {x=3, y=4}
        // temp1, temp2 live only on the method stack
        // After the method exits, the references will disappear -> candidate objects for GC
    }
}

// The Point class is stored in Metaspace
class Point {

    int x; // the object field is on the heap, inside the object block
    int y;

    Point(int x, int y) {
        // Stack: constructor parameters x,y
        this.x = x; // assign to the object field on the heap
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point{" + x + "," + y + "}";
    }
}

   
  

Java SoftReference Cache Example: Explanation

This example demonstrates the use of SoftReference to cache objects and the garbage collector (GC). Objects without direct references can be deleted, but as long as they have a SoftReference, the JVM can retain them until memory exhaustion occurs.
Key Points
Element Where it's stored Comment
Method Local Variables (p1, temp1, temp2) Stack Frame Live Only within a method. After exiting the method, the frame is deleted, and the references disappear.
Point Objects (new Point(...)) Heap (Eden → Survivor → Old, presumably) Created in Eden. If there are no references to them (or only a SoftReference remains), the GC may delete the object due to low memory conditions.
SoftReference to the object Heap + Static area (cache) The object remains in memory until the JVM decides to collect it due to low memory conditions. The static map is stored in Metaspace/Static area.
Static cache map (cache) Metaspace / Static area It lives for the life of the program; the GC doesn't directly touch the map, only the objects within it.
Example of a stack frame and GC
The createTemporaryPoints() method creates local Point objects (temp1, temp2) on the stack. After the method returns, the references disappear, and the objects become candidates for GC.

SoftReference allows "keeping an object in memory until memory exhaustion", even if there are no direct references.

Example with JVM, JIT, and Code Cache


public class JITCodeCacheExample {

    public static void main(String[] args) {

        System.out.println("=== Start of main ===");

        // Create an object, the reference is stored on the main stack
        Point p = new Point(2, 3);
        // Heap: Point object {x=2, y=3}
        // Stack main: p -> Point

        int result = 0;

        // JIT warm-up loop: The JVM considers the hot method after several calls
        for (int i = 0; i < 1_000_000; i++) {
            result += multiplyPoint(p, i);
            // multiplyPoint is called multiple times
            // First, the interpreter executes the bytecode
            // When the JVM finds the method hot, it compiles to Code Cache (native code)
            // Further calls go directly to Code Cache
        }

        System.out.println("Result: " + result);

        // Nullify the reference
        p = null;

        // Heap: the Point object becomes a candidate for GC
        // Code Cache: multiplyPoint remains in memory, the GC does not touch Code Cache

        System.out.println("=== End of main ===");
    }

    // Method that will be compiled to Code Cache after "warming up"
    static int multiplyPoint(Point pt, int factor) {
        // Stack Frame is created on each call
        return pt.x * pt.y * factor;
        // The pt.x and pt.y fields are read from the Heap
        // The method instructions themselves are stored in the Code Cache after the JIT
    }
}

// The Point class is stored in the Metaspace
class Point {

    int x;
    int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point{" + x + "," + y + "}";
    }
}

This example demonstrates how the JVM's JIT compiler works: after multiple calls, the multiplyPoint method is compiled to native code and stored in the Code Cache, while objects and references remain in the Heap/Stack.
Where what is stored
Element Where is it stored Comment
Local variable p Stack Lives while the main frame is active
Object Point Heap (Eden → Survivor → Old) Lives while there are reachable references; GC manages deletion
Method multiplyPoint (bytecode) Metaspace / Class Area Lives while the class is loaded
multiplyPoint method (JIT native code) Code Cache Lives until the Code Cache is freed, not affected by the GC
Warm-up Cycle Stack + Heap Each call creates a Stack Frame; objects on the Heap; After JIT, subsequent calls go through the Code Cache.
Key Points
  • The method is executed through the interpreter until the JVM marks it as hot.
  • After multiple calls, the method is compiled into the Code Cache → super-fast execution.
  • The Code Cache is separate from the Heap and is not touched by the GC.
  • Objects in the Heap live as long as there are reachable references and can be collected by the GC.
  • Stack frames are created for each method call and deleted after return.
Code Cache = "super-fast JVM binary code", Heap = objects, Stack = method frames, Metaspace = bytecode/classes.

🌐 На русском
Total Likes:0
My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Go vs. Java - Comparing Memory Models - Part 2: Atomic Operations, Preemption, Defer/Finally, Context, Escape Analysis, GC, False Sharing
Atomic operations Atomic operations ensure correct execution of variable operations without race conditions, guaranteeing a happens-before between reads and writes. Go example: import "sync/atomic" va...
Java Under the Microscope: Stack, Heap, and GC in Sample Code
Diagram - Java Memory Model - Heap / Non-Heap / Stack Heap (memory for objects) Creates objects using new. Young Generation: Eden + Survivor. Old Generation: Objects that have survived multiple G...
Understanding Multithreading in Java Through Collections and Atomics
1️⃣ HashMap / TreeMap / TreeSet (not thread-safe) HashMap: Structure: array of buckets + linked lists / trees (for collisions). Under the hood: put/remove modifies the bucket array and possibly reord...

New Articles:

Generics, Reflection and Channels - Go vs Java | Types - Language
In this article we will analyze advanced type system features in Go: generics (type parameters), reflection, and channel types for concurrency. We will compare Go and Java approaches, so Java develope...
Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Fullscreen image