Appearance
Lesson 03 · JVM Awareness
Beyond the 1Z0-830 exam
You don't need JVM internals to pass the exam, but a working developer needs enough of a mental model to reason about memory, performance, and the odd OutOfMemoryError or ClassNotFoundException. This is intuition, not tuning — just enough to make good design choices and read a stack trace.
Objectives
After this lesson you will be able to:
- Distinguish the stack (per-thread frames, locals) from the heap (shared objects).
- Explain garbage collection at a high level: reachability, roots, generations.
- Describe class loading and the common errors it produces.
- Apply a few realistic performance intuitions (and avoid premature optimization).
Stack vs heap
| Stack | Heap | |
|---|---|---|
| Holds | Method frames: locals, operand stack, return addresses | All objects and arrays |
| Scope | One per thread | Shared across all threads |
| Lifetime | Pops when the method returns | Until unreachable, then GC'd |
| Errors | StackOverflowError (deep/infinite recursion) | OutOfMemoryError: Java heap space |
A local variable of a reference type lives on the stack, but it holds a reference to an object on the heap. Primitives declared as locals live entirely on the stack.
java
void m() {
int x = 42; // x (the int) is on this frame's stack
String s = new String("hi"); // s (the reference) on the stack; the String object on the heap
} // frame pops; the String is now eligible for GC if unreferencedGotcha
Deep or unbounded recursion throws StackOverflowError, not OutOfMemoryError. Allocating too many live objects throws OutOfMemoryError: Java heap space. They're different failures with different fixes (bound the recursion vs. reduce/stream the data).
Garbage collection — reachability, not reference counting
The GC reclaims objects that are no longer reachable from a GC root (live thread stacks, static fields, JNI handles). You never call free; you just drop references.
- Setting a reference to
nullhelps only if it was the last reachable reference to that object. - An object referenced from a still-live collection (a cache, a
staticlist) is not collected — that's the usual shape of a Java "memory leak." - Most JVMs use generational collection: new objects in a young generation (cheap, frequent collections); long-lived ones get promoted to the old generation. This works because most objects die young.
System.gc()is only a hint — don't rely on it; never put it in production code.
SDET note
A test that holds objects in a static collection (or a thread-local that's never cleared) leaks across tests and can blow the heap in a long suite. "It passes alone but the full run OOMs" usually means shared static state — clear it in teardown (Module 14/15).
Class loading
Classes are loaded lazily, on first active use, by a delegating chain of class loaders (bootstrap → platform → application/classpath). Loading does: load the bytecode → link (verify, prepare, resolve) → initialize (run static initializers and static field assignments).
Common errors:
| Error | Typical cause |
|---|---|
ClassNotFoundException | Reflective load (Class.forName) of a class not on the classpath |
NoClassDefFoundError | Class was present at compile time but missing/failed to init at runtime |
ExceptionInInitializerError | A static initializer threw while the class was being initialized |
Static initialization runs once, the first time the class is initialized — a handy, thread-safe lazy-singleton mechanism (the "initialization-on-demand holder" idiom).
Performance intuition (without premature optimization)
- Measure before optimizing. Reach for a profiler/JMH, not a guess; the bottleneck is rarely where you think.
- Allocation is cheap, but not free. Avoid needless garbage in hot loops (e.g. string
+in a tight loop →StringBuilder; reuse buffers). Elsewhere, prefer clarity. - Big-O beats micro-tuning. An
O(n²)contains-in-a-Listloop dwarfs any constant-factor trick; pick the right data structure (aHashSet) first. - The JIT compiles hot methods to native code after warm-up — which is exactly why a microbenchmark that ignores warm-up lies. Benchmark with a real harness.
Beyond the exam
This whole lesson is background intuition, not exam material — but it's the difference between "the code compiles" and "the code scales." When reviewing AI-generated code (Module 21), JVM awareness is what flags an accidental O(n²), an unbounded cache, or a System.gc() call.
Key Takeaways
- Stack = per-thread frames (locals, primitives, references); heap = shared objects. Deep recursion →
StackOverflowError; too many live objects →OutOfMemoryError. - The GC reclaims unreachable objects from GC roots;
null-ing helps only if it was the last reference. Leaks are usually live references in caches/statics. - Generational GC exploits "most objects die young";
System.gc()is just a hint. - Classes load lazily and initialize once; know
ClassNotFoundExceptionvsNoClassDefFoundErrorvsExceptionInInitializerError. - Measure first; fix Big-O and data structures before micro-optimizing, and account for JIT warm-up when benchmarking.
Lesson Quiz
Where do a thread's local variables and call frames live?
Infinite recursion most likely throws…
When is an object eligible for garbage collection?
A class compiled fine but at runtime you get NoClassDefFoundError. What does that suggest?
Best first step when code is too slow?
Next: Module 13 · Build, Tooling & Ecosystem. Run this module's code in labs/src/main/java/com/jse21/m12_design/.