Quiz 2

Java Memory Model — Stack, Heap, and Garbage Collection

2581 words
13 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# Java Memory Model — Stack, Heap, and Garbage Collection ## 🎯 Learning Objectives By the end of this topic, you will be able to: - Explain the difference between stack and heap memory - Trace activation records (stack frames) during method calls - Understand how objects are allocated on the heap and referenced via...

Java Memory Model — Stack, Heap, and Garbage Collection

🎯 Learning Objectives

By the end of this topic, you will be able to:
  • Explain the difference between stack and heap memory
  • Trace activation records (stack frames) during method calls
  • Understand how objects are allocated on the heap and referenced via the stack
  • Explain how garbage collection works and what triggers it
  • Distinguish between pass-by-value for primitives and pass-by-value-of-reference for objects

📋 Prerequisites

  • Data Types & Operators (02-data-types-operators.md): Primitives vs references
  • BSCS1002 — Python: Python's memory model (everything is an object reference)

1. Intuition: The Cafeteria Tray Analogy

1.1 What Problem Does This Solve?

Imagine you're in a busy cafeteria. You have a tray (the stack) that you hold in your hands — it's small, fast to access, and you add/remove items in a last-in-first-out order (plate on top, eat it, remove it). The stack is your immediate workspace. The kitchen fridge (the heap) is large, shared by everyone, and holds ingredients that anyone can use. But finding something in the fridge takes longer. In programming:
  • Stack: Local variables, method calls — fast, fixed-size, automatically cleaned up
  • Heap: Objects (instances of classes) — larger, flexible, needs garbage collection
Why it matters: Understanding memory explains why Java passes arguments the way it does, why recursion can overflow, and why you don't need to manually free memory (unlike C/C++).

2. The Stack — Your Program's Scratch Pad

2.1 What is the Stack?

The call stack (or just "stack") is a region of memory that stores:
  • Local variables (primitives and references)
  • Method parameters
  • Return addresses (where to go after a method finishes)
  • Intermediate computation results Each time a method is called, a stack frame (activation record) is pushed onto the stack. When the method returns, its frame is popped off.

2.2 Stack Frame Contents

(Diagram)

2.3 Example: Tracing the Stack

java
public class StackDemo {
    public static void main(String[] args) {
        int x = 10;            // main frame: x=10
        int y = 20;            // main frame: x=10, y=20
        int sum = add(x, y);   // Push add frame, pop, store result
        System.out.println(sum);
    }
    static int add(int a, int b) {
        int result = a + b;    // add frame: a=10, b=20, result=30
        return result;         // Pop add frame
    }
}
Step-by-step execution:
  1. JVM calls main → pushes main frame with args
  2. x=10, y=20 are stored in main's frame
  3. add(x,y) is called → pushes add frame with a=10, b=20
  4. Inside add, result=30 is computed
  5. return result → pops add frame, value 30 is returned
  6. sum=30 stored in main's frame
  7. main finishes → pops main frame, program ends

2.4 Stack Overflow — When the Stack Gets Too Full

java
public class InfiniteRecursion {
    public static void main(String[] args) {
        recurse();  // Eventually: StackOverflowError
    }
    static void recurse() {
        recurse();  // Never returns — keeps pushing frames
    }
}
Each recursive call adds a frame. Eventually the stack runs out of space → StackOverflowError.
Managing stack depth: Recursive algorithms must have a base case that terminates. If you need deep recursion, consider an iterative approach or increase stack size with -Xss.

3. The Heap — Where Objects Live

3.1 What is the Heap?

The heap is a large pool of memory where all Java objects (and arrays) are allocated. Unlike the stack, the heap is:
  • Shared across all threads
  • Longer-lived (objects persist until no longer referenced)
  • Managed by garbage collection (unreferenced objects are automatically freed)

3.2 Object Allocation

java
public class HeapDemo {
    public static void main(String[] args) {
        // 's' is on the stack, "Hello" object is on the heap
        String s = new String("Hello");
        // 'person' is on the stack, Person object is on the heap
        Person person = new Person("Alice", 25);
        int x = 10;  // Primitive — stored entirely on the stack
    }
}
(Diagram)

3.3 The new Keyword

The new keyword is Java's way of saying "allocate memory on the heap":
java
Person p1 = new Person("Alice", 25);
//         ↑      ↑
//     Reference  Object allocated on heap
//
// p1 (stack) ──→ {name="Alice", age=25} (heap)
Every time you write new, a new object is created on the heap. The variable holds a reference (memory address) to that object.

4. Pass-by-Value: The Most Important Java Concept

4.1 Everything is Pass-by-Value

This is the #1 concept students misunderstand. Java is ALWAYS pass-by-value — but the "value" depends on what you're passing:
  • Primitives: The actual value is copied
  • References: The reference (memory address) is copied, NOT the object

4.2 Passing Primitives

java
public class PassPrimitive {
    public static void main(String[] args) {
        int x = 5;
        System.out.println("Before: " + x);  // 5
        changePrimitive(x);
        System.out.println("After: " + x);   // 5 — unchanged!
    }
    static void changePrimitive(int val) {
        val = 10;  // Only changes the copy
    }
}
(Diagram)

4.3 Passing References

java
public class PassReference {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");
        System.out.println("Before: " + sb);  // Hello
        changeReference(sb);
        System.out.println("After: " + sb);   // Hello World — object changed!
    }
    static void changeReference(StringBuilder ref) {
        ref.append(" World");  // Modifies the SAME object
        ref = new StringBuilder("New");  // Creates new object, but doesn't affect caller
    }
}
(Diagram) Key insight: The ref variable is a copy of sb's address. Both point to the same object. Changes via ref affect the object. But reassigning ref to a new object doesn't affect sb.

5. Garbage Collection — Automatic Memory Management

5.1 The Problem

In languages like C/C++, you must manually free memory with free() or delete. Forgetting causes memory leaks (program uses more and more memory). Freeing too early causes dangling pointers (crash when accessing freed memory).

5.2 The Java Solution

Java's garbage collector (GC) automatically frees objects that are no longer reachable. "Reachable" means referenced from the stack (or from another reachable object).
java
public class GCDemo {
    public static void main(String[] args) {
        Person p = new Person("Alice", 25);  // Reachable: p references it
        p = null;  // Object is now UNREACHABLE → eligible for GC
        // GC may or may not collect it immediately
    }
}

5.3 When is an Object Eligible for GC?

  1. Nullified reference: p = null;
  2. Reassignment: p = new Person("Bob", 30); — old object loses reference
  3. Local variable goes out of scope: Object created inside a method is unreachable when the method returns
  4. Islands of isolation: Two objects reference each other but no external reference exists (Diagram) Even though Island 1 and Island 2 reference each other, no one outside references either → both are eligible for GC.

5.4 Generational GC

Modern JVMs use a generational approach: (Diagram)
  • Young Generation: New objects. Most die young (short-lived). Collected frequently (Minor GC).
  • Old Generation: Objects that survived many GC cycles. Collected less frequently (Major GC).
  • Eden + Survivor spaces: Objects start in Eden, survive to Survivor, then get promoted to Old.

5.5 Cannot Force GC

You can suggest GC with System.gc(), but the JVM is free to ignore it:
java
System.gc();  // "Please run GC" — no guarantee!

6. Memory Leaks in Java

Even with garbage collection, Java can still leak memory:
java
public class MemoryLeakDemo {
    private static List<byte[]> leak = new ArrayList<>();
    public static void main(String[] args) {
        while (true) {
            leak.add(new byte[1024 * 1024]);  // 1 MB each iteration
            // Never removes from list → OutOfMemoryError eventually
        }
    }
}
Common leak causes:
  1. Forgotten references (static collections that grow unbounded)
  2. Unclosed resources (file handles, sockets, DB connections)
  3. Inner class holding reference to outer class (implicit pointer)
  4. String.intern() in unlimited fashion

7. Java vs Python: Memory Model

FeatureJavaPython
PrimitivesStack-allocated (8 types)Everything is an object on heap
Object allocationnew keywordImplicit on creation
Garbage collectionGenerational, automaticReference counting + generational GC
PassingPass-by-value (always)Pass-by-object-reference
Memory managementAutomatic (GC)Automatic (GC with ref counting)
Manual memoryNot possibleNot possible
Stack overflowPossible (deep recursion)Possible (deep recursion)

8. Common Pitfalls

Pitfall 1: Thinking Objects are Passed by Reference

java
public static void swap(Person a, Person b) {
    Person temp = a;
    a = b;           // Only changes local copies!
    b = temp;
}
// Caller's references unchanged!
Why: Java passes the value of the reference, not the reference itself. Changing what the local variable points to doesn't affect the caller.

Pitfall 2: Assuming GC Runs Immediately

java
Person p = new Person("Alice", 25);
p = null;
System.out.println("Object should be gone now...");  // Still there!
Why: GC runs when the JVM decides it's necessary, not immediately when references are nullified.

Pitfall 3: Creating Too Many Objects in a Loop

java
for (int i = 0; i < 1_000_000; i++) {
    String s = new String("Item " + i);  // Creates millions of objects
    // Use StringBuilder instead for string concatenation in loops
}
Why: Each iteration creates new objects on the heap, stressing GC. Use StringBuilder for loop-based string construction.

Pitfall 4: Stack Overflow from Missing Base Case

java
static int factorial(int n) {
    return n * factorial(n - 1);  // No base case! StackOverflowError
}
Why: Without if (n <= 1) return 1;, recursion never terminates and stack frames pile up until overflow.

9. Practice Questions

Q1: Explain what happens in memory when this code executes
java
public class MemoryDemo {
    public static void main(String[] args) {
        int a = 5;
        int b = 10;
        int result = add(a, b);
    }

    static int add(int x, int y) {
        int sum = x + y;
        return sum;
    }
}
Answer:
  1. main frame pushed: a=5, b=10
  2. add(a,b) called: pushes add frame with x=5, y=10
  3. Inside add: sum=15 computed and stored in add's frame
  4. return sum: add's frame is popped, value 15 returned
  5. result=15 stored in main's frame
  6. main completes: frame popped, program ends
No heap allocation — all values are primitives on the stack. Q2: Does the following code modify the original object?
java
public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    modify(arr);
    System.out.println(Arrays.toString(arr));
}

static void modify(int[] array) {
    array[0] = 99;
}
Answer: Yes! Output is [99, 2, 3].
Reasoning: arr is a reference to an array object on the heap. The modify method receives a copy of this reference. Both the original and the copy point to the SAME array object. Changing array[0] changes the original array.
However, if modify reassigned array = new int[]{...}, it would NOT affect the original. Q3: When is the StringBuffer object eligible for GC?
java
public class GCDemo {
    public static void main(String[] args) {
        StringBuffer sb1 = new StringBuffer("Hello");
        StringBuffer sb2 = new StringBuffer("World");
        sb1 = sb2;
        // Line X
    }
}
Answer: At Line X, the first StringBuffer ("Hello") is eligible for GC because:
  • sb1 originally pointed to the "Hello" object
  • After sb1 = sb2;, sb1 now points to the "World" object
  • No other reference points to "Hello"
  • Therefore it's unreachable → eligible for GC
sb2 still points to "World", and sb1 also points to "World", so "World" is NOT eligible for GC. Q4: Why does this code cause a memory leak?
java
public class LeakExample {
    private static List<Object> list = new ArrayList<>();

    public void addToList(Object obj) {
        list.add(obj);  // Objects added but never removed
    }
}
Answer: The static list holds strong references to all added objects. Since the list is static, it lives for the entire program lifetime. Objects in the list can never be garbage collected unless explicitly removed (list.remove(...)) or the list is nullified.
This is a classic memory leak pattern in Java. Q5: What is the output?
java
public class PassByValueDemo {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 200;
        swap(a, b);
        System.out.println("a = " + a + ", b = " + b);
    }

    static void swap(Integer x, Integer y) {
        Integer temp = x;
        x = y;
        y = temp;
    }
}
Answer: a = 100, b = 200 (unchanged).
Reasoning: Integer is an object, but the swap only changes the local copies x and y of the references. The caller's a and b are unaffected. This proves Java is pass-by-value even for objects. Q6: What is the difference between stack and heap?
Answer:
PropertyStackHeap
SizeFixed (typically ~1MB per thread)Large (grows as needed)
SpeedVery fast (LIFO access)Slower (dynamic allocation)
ContentsPrimitives, references, method framesObjects, arrays
LifetimeMethod execution scopeUntil no references remain
CleanupAutomatic (pop frame)Garbage collection
SharingPer-thread (not shared)Shared across threads
OverflowStackOverflowErrorOutOfMemoryError
Q7: How does generational garbage collection work?
Answer: Modern JVMs divide the heap into generations:
  1. Young Generation: Newly created objects go to Eden space. When Eden fills up, a Minor GC runs. Surviving objects move to a Survivor space.
  2. Survivor spaces (S0, S1): Objects that survive Minor GCs are moved between survivor spaces. Each time an object survives, its "age" increases.
  3. Old Generation: Objects that reach a certain age threshold are promoted to the old/tenured generation. Major GC runs on old generation less frequently.
This design exploits the weak generational hypothesis: most objects die young. Q8: What happens if you don't nullify a reference after use?
java
public List<String> process() {
    List<String> result = new ArrayList<>();
    // ... process ...
    return result;  // Caller keeps reference
}
Answer: If the caller keeps the returned reference, the object is still reachable and not GC'd. If the caller drops the reference, the object becomes eligible for GC.
Nullifying references in local methods is usually unnecessary — the reference goes out of scope when the method returns. However, nullifying can help in long-lived objects (e.g., class fields) where you want to allow GC before the containing object is collected.

📐 Key Concepts

ConceptDescription
StackLIFO structure storing local variables and method frames
HeapMemory pool for all objects and arrays
Stack frameMethod's workspace (parameters, locals, return address)
ReferenceMemory address pointing to a heap object
Pass-by-valueThe argument's value (primitive or reference copy) is passed
GC rootsStack references, static fields — starting points for reachability
Generational GCYoung (Eden, Survivor) + Old generation collection strategy
Memory leakUnintentional retention of references preventing GC

🔗 Cross-References

Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.