Java under the Microscope: Stack, Heap, and GC using Code Examples

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

Heap (memory for objects)
Creates objects via new.
Young Generation: Eden + Survivor.
Old Generation: objects that have survived several GC collections.
Heap size is usually larger than Non-Heap.
Young Generation
Eden
Survivor
Old Generation
Non-Heap
Memory not directly associated with user objects.
Includes Metaspace, Code Cache, Compressed Class Space, 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 Heap keep them alive.
Thread-1
Thread-2
Thread-3

Java Memory Deep Dive: how objects, primitives and methods live in memory


// Класс MemoryExample хранится в Metaspace
public class MemoryExample {

    // Статическая переменная — хранится в Metaspace / Static area
    static int staticVar = 10;
    // Metaspace:
    // MemoryExample.staticVar = 10

    public static void main(String[] args) {

        // Стек main:
        // args -> ссылка на массив строк
        System.out.println("Start of main");

        // Создаём объект Point в куче (Eden), ссылка p хранится в стеке main
        Point p = new Point(5, 7); 
        // Heap (Eden):
        // [Point object header | x=5 | y=7]
        // Стек main: p -> Point в куче

        // Примитивная переменная a хранится в стеке main
        int a = 20; 
        // Стек main: a = 20

        // Вызов метода multiplyPoint с передачей ссылки p и примитива a
        // JVM создаёт новый кадр стека для multiplyPoint
        int result = multiplyPoint(p, a);

        // ---------------- Кадр multiplyPoint ----------------
        // pt -> ссылка на объект Point в куче
        // factor = копия a (20)
        // Операндный стек для вычислений
        // Выполнение: return pt.x * pt.y * factor = 5 * 7 * 20 = 700
        // ----------------------------------------------------
        // После возврата из multiplyPoint кадр удаляется автоматически
        // Стек main теперь снова активен

        System.out.println("Result: " + result);
        // Стек main: печатаем 700

        // Обнуляем ссылку p
        p = null; 
        // Heap: объект Point {x=5, y=7} теперь недостижим
        // JVM может удалить объект автоматически, когда решит, что нужно освободить память
        // Если объект пережил несколько GC → Survivor → Old Generation (предположительно)

        // Создаём новый объект Point
        Point q = new Point(10, 20);
        // Heap (Eden):
        // [Point object header | x=10 | y=20]
        // Стек main: q -> Point в куче

        System.out.println("End of main");
        // После завершения main:
        // Локальные переменные args, result, q очищаются
        // Недостижимые объекты в куче будут собраны GC автоматически
    }

    static int multiplyPoint(Point pt, int factor) {
        // ---------------- Кадр стека multiplyPoint ----------------
        // pt -> ссылка на объект Point в куче
        // factor = копия int
        // Операндный стек JVM выполняет pt.x * pt.y * factor
        // ----------------------------------------------------------
        return pt.x * pt.y * factor;
    }
}

// Класс Point хранится в Metaspace
class Point {
    int x; // поле объекта — в куче
    int y; // поле объекта — в куче

    Point(int x, int y) {
        // Кадр стека конструктора:
        // параметры x, y — локальные копии
        this.x = x; // присваиваем поле объекта в куче
        this.y = y; // присваиваем поле объекта в куче
        // После завершения конструктора кадр удаляется, объект остаётся в куче
    }

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

Java Memory Example: Explanation

This example shows how the JVM allocates objects and variables between the stack, heap, and static area, as well as how objects become candidates for garbage collection (GC).
Key Points
Element Where it's stored Comment
Local method variables (int a, object references) Stack (Stack Frame) Live only within the method. After exiting the method, the frame is removed.
Objects (new Point(...)) Heap (Heap, Young Generation → Survivor → Old) Created in Eden, may move to Survivor and then to Old if they survive several GC collections.
Static variables (staticVar) Metaspace / Static area Live for the lifetime of the program, GC does not touch them.
Objects without references (e.g., after p = null) Heap (in Eden/Survivor/Old) Unreachable objects become candidates for GC and will be automatically removed by the JVM.
Stack Frame Example
The method multiplyPoint(pt, factor) creates a stack frame that contains:
  • A reference pt to the Point object in 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 life of objects in memory


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

public class CacheGCExample {

    // Static cache map — lives in Metaspace/Static area
    static HashMap<String, SoftReference<Point>> cache = new HashMap<>();

    public static void main(String[] args) {

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

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

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

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

        // Calling a method that creates temporary objects
        createTemporaryPoints();
        // Inside the method, stack frame multiplyPoint: temporary objects are removed after exit
        // Heap: objects that have no references are candidate for GC

        // Forced garbage collector call (for demonstration purposes only)
        System.gc();
        System.out.println("=== After System.gc() ===");

        // Checking the cache
        SoftReference<Point> ref = cache.get("first");
        Point cachedPoint = ref.get(); // may return null if GC 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 in the stack of the method
        // After exiting the method, the references will disappear -> objects candidate for GC
    }
}

// Class Point is stored in Metaspace
class Point {
    int x; // object field — in heap, inside the object block
    int y;

    Point(int x, int y) {
        // Stack: constructor parameters x,y
        this.x = x; // assigning to the object's field in heap
        this.y = y;
    }

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

Java SoftReference Cache Example: Explanation

This example demonstrates the use of SoftReference for caching objects and the workings of the garbage collector (GC). Objects without strong references can be removed, but as long as there is a SoftReference to them, the JVM may keep them until memory runs low.
Key Points
Element Where Stored Comment
Method local variables (p1, temp1, temp2) Stack (Stack Frame) Exist only within the method. After exiting the method, the frame is removed, and references are gone.
Point objects (new Point(...)) Heap (Eden → Survivor → Old, presumably) Created in Eden. If there are no references to them (or only a SoftReference remains), GC may remove the object when memory runs low.
SoftReference to an object Heap + Static area (cache) The object is kept in memory until the JVM decides to collect it when memory is low. The static map is stored in Metaspace/Static area.
Static cache map (cache) Metaspace / Static area Lives for the lifetime of the program; the GC does not touch the map directly, only the objects within it.
Example of Stack Frame and GC
The method createTemporaryPoints() creates local Point objects (temp1, temp2) on the stack. After exiting the method, the references are gone, and the objects become candidates for GC.

SoftReference allows “to keep the object in memory until memory is low”, even if there are no strong references.

JIT Compiler with JVM, JIT and Code Cache


public class JITCodeCacheExample {

    public static void main(String[] args) {

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

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

        int result = 0;

        // Loop to warm up JIT: JVM considers the method hot after several calls
        for (int i = 0; i < 1_000_000; i++) {
            result += multiplyPoint(p, i);
            // multiplyPoint is called repeatedly
            // Initially, the interpreter executes the bytecode
            // When JVM considers the method hot → compiles to Code Cache (native code)
            // Subsequent 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, GC does not touch Code Cache

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

    // Method that will be compiled into Code Cache after "warming up"
    static int multiplyPoint(Point pt, int factor) {
        // Stack Frame is created with each call
        return pt.x * pt.y * factor;
        // Fields pt.x, pt.y are read from Heap
        // Instructions of the method itself after JIT lie in Code Cache
    }
}

// Class Point is stored in 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 the operation of the JVM JIT compiler: the method multiplyPoint after multiple calls is compiled into native code and stored in Code Cache, while objects and references remain in Heap/Stack.
Where is what stored
Element Where is it stored Comment
Local variable p Stack Lives as long as the main frame is active
Object Point Heap (Eden → Survivor → Old) Lives as long as there are reachable references, GC manages deletion
Method multiplyPoint (bytecode) Metaspace / Class Area Lives as long as the class is loaded
Method multiplyPoint (JIT native code) Code Cache Lives until Code Cache is freed, GC does not affect it
Warm-up loop Stack + Heap Each call creates a Stack Frame; objects on Heap; after JIT subsequent calls go through Code Cache
Main points
  • The method is executed through the interpreter until the JVM marks it as hot.
  • After multiple calls, the method is compiled into Code Cache → super fast execution.
  • Code Cache is separate from Heap; GC does not touch it.
  • Objects in Heap live as long as there are reachable references and can be collected by GC.
  • Stack frames are created for each method call and removed after return.
Code Cache = "super fast binary code of JVM", 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:

Concurrency is not about “starting many threads”. It’s about agreements between them. Imagine a restaurant kitchen: — cooks (threads / goroutines) — orders (tasks) — and the main question: how do th...
Struct, methods and interfaces in Go vs Java | Types - Language
Series: Go for Java Developers — exploring struct, interface, receiver types and type embedding In this article, we will examine how types architecture is built in Go. For a Java developer, this is e...
Scheduler internals in Go ↔ Java: how your code is actually executed
When you write go func() or create a Thread in Java, it seems like you are managing concurrency. But in reality, you are passing the task to the scheduler. And this is where the real show begins. Go...

New Articles:

Concurrency is not about “starting many threads”. It’s about agreements between them. Imagine a restaurant kitchen: — cooks (threads / goroutines) — orders (tasks) — and the main question: how do th...
When HashMap starts killing production: the engineering story of ConcurrentHashMap
Imagine a typical production service. 32 CPU hundreds of threads configuration / session / rate limits cache tens of thousands of operations per second And somewhere inside — a regular Map. At first...
Zero Allocation in Java: what it is and why it matters
Zero Allocation — is an approach to writing code in which no unnecessary objects are created in heap memory during runtime. The main idea: fewer objects → less GC → higher stability and performance. ...
Fullscreen image