Step 1 of 7 · Reading · ~3 min
Learn
Reflection and JVM
JVM Memory and GC Basics
You will be able to say which region of the JVM a piece of data lives in, which objects the garbage collector may free and why, and which error you get when each region runs out. The exercise has you do the first step of a collector by hand: find what is still reachable.
The regions
- Heap: every object made with
new. It is split into a young generation (new objects, collected often and cheaply) and an old generation (objects that survived several young collections). - Thread stacks: one per thread, holding a frame for each active method call: local primitives and references. Its size is
-Xss. - Metaspace: class metadata (it replaced PermGen in Java 8).
- Native memory: direct
ByteBuffers, JIT code, thread structures. The heap flags do not limit it.
Two limits fail differently. Too many live objects gives OutOfMemoryError: Java heap space (cap set by -Xmx). Too many nested calls gives StackOverflowError (cap set by -Xss). Raising -Xmx does nothing for a stack overflow.
What the collector may free
Java does not count references. An object is live if some chain of references leads to it from a GC root; everything else is garbage, even if garbage objects point at each other. The roots are:
- local variables and parameters in the frames of running threads
- static fields of loaded classes
- live threads themselves, and references held by native code
The collector marks everything reachable from the roots and reclaims the rest. A worked case on a different graph: objects P, Q, R, S, T, where P -> Q, Q -> R, S -> T, T -> S, and the only root is a local variable holding P. Marking reaches P, Q, R. S and T form a cycle, but nothing live reaches them, so both are garbage. A reference-counting scheme would leak them (each has count 1); a tracing collector does not. Direction matters: if S also pointed at P, that would not make S live.
Generations and collectors
Most objects die young. New objects go to eden; when it fills, a minor GC copies the survivors to a survivor space, and objects that survive enough rounds are promoted to the old generation. A full GC collects everything and pauses longest. G1 has been the default since Java 9; it splits the heap into regions and aims for a pause target (-XX:MaxGCPauseMillis). ZGC and Shenandoah target very short pauses on big heaps. System.gc() is only a request.
Reference strength
A normal reference is strong. A SoftReference is cleared only under memory pressure (caches). A WeakReference is cleared at the next collection once nothing strong reaches the object (WeakHashMap, canonicalising maps). finalize() is deprecated; use try-with-resources or Cleaner.
Leaks in a garbage-collected language
A leak is an object that is reachable but never used again: a static List that only grows, a listener never removed, a ThreadLocal in a pooled thread. Take a heap dump (jcmd <pid> GC.heap_dump file.hprof) and follow the path from a GC root to the leaked object.
Walking a long chain without the stack
A recursive walk uses one stack frame per link, so a chain of 100,000 nodes ends in StackOverflowError even though the heap is nearly empty. The cure is to keep the pending nodes on the heap yourself:
Deque<Node> pending = new ArrayDeque<>();
pending.push(first);
while (!pending.isEmpty()) {
Node n = pending.pop();
total += n.weight;
if (n.next != null) pending.push(n.next);
}
Common mistakes
- Marking recursively. It passes small tests and dies with
StackOverflowErroron a long chain. - Not recording visited objects. A cycle makes the walk run forever.
- Treating a reference out of a garbage object as a root. Only the roots start the walk.
- Reading
-Xmxas a stack limit. The heap and stack limits are separate.
Your exercise
Implement the mark phase over a small object graph and report what a collector would keep and free. The graph is built for you; you return which objects are reachable.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…