Table of Contents:
- Diagram - Java Memory Model - Heap / Non-Heap / Stack
- Java Memory Deep Dive: How Objects, Primitives, and Methods Live in Memory
- Java Memory Example: Explanation
- Java Caching and GC: How SoftReference Affects the Lifespan of Objects in Memory
- Java SoftReference Cache Example: Explanation
- Example with JVM, JIT, and Code Cache
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
Young Generation: Eden + Survivor.
Old Generation: Objects that have survived multiple GC collections.
The Heap size is usually larger than the Non-Heap size.
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).
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.
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).
| 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. |
The multiplyPoint(pt, factor) method creates a stack frame that contains:After return, the frame is removed, and the result is returned to the calling method.
- A reference pt to a Point object on the heap
- A local variable factor
- The JVM operand stack for calculations
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. |
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. |
- 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.
Gallery
My social media channel
Useful Articles:
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...
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...
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:
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...
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...
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...