🌍
Why does this topic matter?
Memory management is invisible — until it crashes your program.
Understanding where a variable lives (stack vs heap)
explains every "surprising" Java behavior you will ever encounter:
why primitives are copied but objects are shared, why passing an
object to a method can change it while passing an int never can, and
why a missing null check causes a
NullPointerException. This knowledge also directly
impacts how you reason about the space complexity of your
algorithms.
📖 Before We Start — The Big Picture
Every Java program runs inside the
Java Virtual Machine (JVM) — a software layer that
sits between your code and the real hardware. The JVM manages all
memory on your behalf, splitting it into distinct regions, each with a
specific job.
Stack = a small, fast notepad on your desk. Each
method call writes its local variables here. When the method returns,
the page is torn off and thrown away. Heap = a large
warehouse. Every object you create with new is stored
here and stays until nobody is referencing it anymore. The
Garbage Collector is the warehouse cleaner — it
periodically sweeps away objects nobody is using.
🔗
How it connects: Lecture 1 taught you
what types exist. This lecture explains where they
live in memory. Lecture 3 (OOP) builds on this — every object you
create goes onto the heap, and understanding that is why
== compares addresses, not values.
🏗 JVM Architecture
📌
The Mental Model: The JVM manages all memory for
your program in distinct regions. Understanding these regions
explains why Java behaves differently from C/C++ and helps you
reason about performance and bugs.
┌──────────────────────── JVM MEMORY ────────────────────────┐ │ HEAP
(all threads share this) │ │ ┌───────────────┐ ┌───────────────────┐
┌──────────┐ │ │ │ YOUNG GEN │ │ OLD GEN │ │ METASPACE│ │ │ │ Eden
|S0|S1 │ │ (long-lived objs) │ │ Classes │ │ │ │ new objs │ │ tenured
≥15 GCs │ │ Statics │ │ │ └───────────────┘ └───────────────────┘
└──────────┘ │
├────────────────────────────────────────────────────────────┤ │ STACK
(one per thread — holds method frames) │ │ ┌───────────────┐
┌───────────────┐ ┌───────────┐ │ │ │ Frame:main │ │ Frame:sort │ │
Frame:cmp │ ← TOP │ │ │ local vars │ │ local vars │ │ a,b,ret │ │ │
└───────────────┘ └───────────────┘ └───────────┘ │
└────────────────────────────────────────────────────────────┘
Stack vs Heap Quick Reference
| Property |
Stack |
Heap |
| What lives here |
Primitives, reference variables (pointer itself), frames
|
All objects, arrays |
| Size |
Small ~1 MB/thread |
Large (configured via -Xmx) |
| Thread safety |
✅ Private per thread |
❌ Shared — needs sync |
| Speed |
⚡ Very fast (LIFO pointer) |
Slower (GC pressure) |
| Error |
StackOverflowError |
OutOfMemoryError |
int x = 42; // x lives on STACK (primitive)
String s = "hi"; // s (ref) on STACK → String object on HEAP
int[] arr = {1,2,3}; // arr (ref) on STACK → int[3] on HEAP
// Reference copy ≠ object copy!
int[] a = {1,2,3};
int[] b = a; // b points to SAME array
b[0] = 99;
System.out.println(a[0]); // → 99! Same heap array
// Deep copy needed for independence:
int[] c = Arrays.copyOf(a, a.length);
🔡 String Pool & Interning
String a = "hello"; // Pool["hello"] → 0xAAA
String b = "hello"; // same Pool object 0xAAA
System.out.println(a == b); // → true
String c = new String("hello");// bypasses pool → new heap obj
System.out.println(a == c); // → false (different refs)
System.out.println(a.equals(c)); // → true (same content)
String d = c.intern(); // force into pool
System.out.println(a == d); // → true
// RULE: ALWAYS use .equals() for string comparison. Never ==.
🗑 Garbage Collection
💡
Generational Hypothesis: Most objects die young. GC
exploits this by collecting the small Young Generation frequently
(Minor GC — fast) and the Old Generation rarely (Major GC — slow
Stop-the-World).
🗂 JCF Masterclass
| Class |
Backing |
get |
add/put |
remove |
Order |
| ArrayList |
Object[] |
O(1) |
O(1)* |
O(n) |
Insertion |
| LinkedList |
Doubly linked |
O(n) |
O(1) ends |
O(1) ends |
Insertion |
| ArrayDeque |
Circular array |
O(1) ends |
O(1)* |
O(1) ends |
Insertion |
| PriorityQueue |
Binary heap |
O(1) peek |
O(log n) |
O(log n) |
Priority |
| HashSet |
HashMap |
— |
O(1) avg |
O(1) avg |
None |
| TreeSet |
Red-Black |
O(log n) |
O(log n) |
O(log n) |
Sorted |
| HashMap |
Array+list/tree |
O(1) avg |
O(1) avg |
O(1) avg |
None |
| LinkedHashMap |
HashMap+list |
O(1) avg |
O(1) avg |
O(1) avg |
Insertion |
| TreeMap |
Red-Black |
O(log n) |
O(log n) |
O(log n) |
Sorted key |
HashMap Internals
🔍
Bucket index = hash(key) & (n-1).
Collisions form a linked list per bucket. When chain length ≥ 8 it
treeifies to a Red-Black Tree (O(log n) worst case). Doubles
capacity when size > capacity × 0.75.
When to Use Which
| Need |
Use |
| Frequency count |
HashMap<T,Integer> + getOrDefault |
| Visited set (BFS/DFS) |
HashSet<T> |
| Min/Max always available |
PriorityQueue (min-heap default) |
| Stack or Queue |
ArrayDeque (faster than Stack class) |
| LRU Cache |
LinkedHashMap with accessOrder=true |
| Floor/Ceiling key queries |
TreeMap |
💪 Practice Problems
Problem
Group strings that are anagrams of each other.
["eat","tea","tan","ate","nat","bat"] →
[["bat"],["nat","tan"],["ate","eat","tea"]]
►Solution
List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> map = new HashMap<>();
for (String s : strs) {
char[] ch = s.toCharArray(); Arrays.sort(ch);
map.computeIfAbsent(new String(ch), k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
// "eat"→sort→"aet", "tea"→sort→"aet" → same group!
TimeO(n·k log k)
SpaceO(n·k)
Problem
Return the k most frequent elements from an array.
[1,1,1,2,2,3], k=2 → [1,2]
►Solution
int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> freq = new HashMap<>();
for (int n:nums) freq.merge(n,1,Integer::sum);
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b)->a[1]-b[1]);
for (var e:freq.entrySet()) {
pq.offer(new int[]{e.getKey(),e.getValue()});
if(pq.size()>k) pq.poll();
}
int[] res=new int[k];
for(int i=k-1;i>=0;i--) res[i]=pq.poll()[0];
return res;
}
Problem
Design LRU Cache with O(1) get and put. Evict least recently
used when over capacity.
►Solution
class LRUCache extends LinkedHashMap<Integer,Integer> {
final int cap;
LRUCache(int c) { super(c,0.75f,true); cap=c; } // true=accessOrder
public int get(int k) { return super.getOrDefault(k,-1); }
public void put(int k,int v) { super.put(k,v); }
@Override protected boolean removeEldestEntry(Map.Entry e) { return size()>cap; }
}
TimeO(1) all
SpaceO(capacity)
Problem
Given array nums and target, return indices of the
two numbers that add up to target.
[2,7,11,15], target=9 → [0,1]
Think First
For each element nums[i], the
complement we need is target - nums[i].
Store each number and its index in a HashMap. Before inserting,
check if the complement already exists — if so, we're done.
►Solution + Trace
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (seen.containsKey(need))
return new int[] { seen.get(need), i };
seen.put(nums[i], i);
}
return new int[] {};
}
// [2,7,11,15] t=9:
// i=0: need=7, not in map → put {2:0}
// i=1: need=2, found at 0 → return [0,1] ✓
Problem
Given a string of brackets, return true if every
open bracket has a matching close bracket in the correct order.
"()[]{}" → true "([)]" → false "{[]}"
→ true
Think First
Push open brackets onto a Stack. When you see a close bracket,
the Stack's top must be the matching open bracket — if not, it's
invalid. After scanning, the stack must be empty (every open was
closed).
►Solution
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') stack.push(c);
else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
return stack.isEmpty();
}
Problem
Design a stack that supports push,
pop, top, and getMin —
all in O(1) time.
push(5)→push(3)→push(7)→getMin()=3, pop()→getMin()=3,
pop()→getMin()=5
Think First
Use two stacks: a main stack for all values,
and a min-stack that tracks the current minimum at each
level. When you push a value, also push the new minimum. When
you pop, pop from both. getMin() is always the top
of the min-stack.
►Solution
class MinStack {
Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> minStack = new ArrayDeque<>();
public void push(int val) {
stack.push(val);
int curMin = minStack.isEmpty() ? val : Math.min(val, minStack.peek());
minStack.push(curMin);
}
public void pop() { stack.pop(); minStack.pop(); }
public int top() { return stack.peek(); }
public int getMin(){ return minStack.peek(); }
}
// push(5): stack=[5], min=[5]
// push(3): stack=[3,5], min=[3,5]
// push(7): stack=[7,3,5], min=[3,3,5]
// getMin()=3 ✓ pop()→stack=[3,5],min=[3,5] → getMin()=3 ✓
TimeO(1) all ops
SpaceO(N)
Problem
Given unsorted array, find the length of the longest sequence of
consecutive integers. Must run in O(N).
[100,4,200,1,3,2] → 4 (sequence: 1,2,3,4)
Think First
Load all numbers into a HashSet. For each number,
only start counting if it's the start of a sequence
(i.e., n-1 is NOT in the set). Then count
consecutive numbers n+1, n+2, ... until the chain
breaks. This is O(N) because each number is visited at most
twice.
►Solution + Trace
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int n : nums) set.add(n);
int best = 0;
for (int n : set) {
if (!set.contains(n - 1)) { // start of a sequence
int cur = n, len = 1;
while (set.contains(++cur)) len++;
best = Math.max(best, len);
}
}
return best;
}
// [100,4,200,1,3,2]: set has all values
// n=1: 0 not in set → start! count 1,2,3,4 → len=4 → best=4 ✓
📝 Assignment
📋
Lecture 2 Assignment25+ problems: JVM memory
quiz, String pool traps, HashMap patterns, LRU Cache, Top-K, Group
Anagrams, and heap operation practice.
Open Assignment →
✅ Completion Checklist
✓
I can draw the JVM memory layout from scratch (Stack, Eden, S0/S1,
Old Gen, Metaspace)
✓
I know what lives on Stack vs Heap and can trace any code snippet
✓
I know why new String("x") == "x" is false
✓
I can explain GC eligibility and what a memory leak looks like in
Java
✓
I know time complexity of HashMap, TreeMap, PriorityQueue,
ArrayDeque
✓
I understand how HashMap computes bucket index and handles
collisions
✓
I can implement LRU Cache using LinkedHashMap in under 10 min
✓
I can solve Top-K using HashMap + PriorityQueue
✓
I know when to use TreeMap.floorKey() vs HashMap
✓
I understand the treeify threshold (8) and load factor (0.75) in
HashMap
🧠
You're ready for Topic 3: OOP & Collections
You've mastered Java Memory — the JVM Stack & Heap, garbage
collection, and String interning. OOP is where Java's power truly
unlocks. Get ready for inheritance, polymorphism, generics, and Java
Collections.