skip to content

ClassLoader Leaks

A classloader stays alive as long as anything reachable points at one of its classes, instances or static fields, pinning all of them in metaspace. This is why repeated hot redeploys on an application server end in OutOfMemoryError: Metaspace, a favorite senior debugging story.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

Explain the reference chain that keeps a classloader alive, and why a leak is 'all-or-nothing' for its classes.

level: seniorimportance: must knowfreq 40%

answer

  1. Four edges: instance→Class, Class→ClassLoader, ClassLoader→its classes, Class→statics
  2. GC reachability makes the bundle one unit
  3. One strong reference pins the whole loader
  4. Diagnose via heap dump 'path to GC roots'
  5. Weak references don't pin the loader

basics

~20 s

Every object points to its Class, every Class points to the classloader that loaded it, and the classloader points to all the classes it loaded. So holding one live object keeps the loader and every class it loaded alive in metaspace — you can't lose just part of it.

solid answer

~50 s

The JVM links objects, classes, and loaders in a cycle that the GC sees as one unit. An instance holds a reference to its `Class` object (`getClass()`); the `Class` holds a reference to its defining `ClassLoader` (`getClassLoader()`); and the `ClassLoader` retains references to every class it has defined. Static fields are stored on the `Class`, so they're reachable too. Because of this, if anything outside keeps any one of these reachable — a single instance, one `Class`, one static field, or the loader itself — the GC marks the whole classloader and *all* of its loaded classes as reachable. That's why a classloader leak is all-or-nothing: you don't leak one class's metadata, you pin the entire loader's namespace in metaspace. To fix a leak you must locate the GC-root path into this graph (often via a heap dump's path-to-GC-roots) and sever it.

go deeper

for a junior

Knows an object 'knows its class' and classes live in metaspace; likely can't trace the full chain.

for a middle

Can name the instance→Class→ClassLoader chain and that it keeps the loader alive.

for a senior

States all four edges, explains all-or-nothing reachability, and knows to use a heap dump path-to-GC-roots to find the offending reference.

for a principal

Reasons about mitigation strategies (weak references in shared registries, loader isolation) and trade-offs at the framework/container level.

## The objects involved Three runtime entities matter: 1. **Instances** — ordinary objects on the heap. 2. **`Class` objects** — one per loaded class; they hold the class's metadata, its static fields, and method/bytecode references. These largely live in metaspace (native memory), though a `java.lang.Class` *mirror* object exists on the heap. 3. **`ClassLoader` objects** — heap objects that loaded classes. ## The reference edges (memorize these four) - **instance → Class**: every object knows its type; `obj.getClass()` returns the `Class`. The JVM stores a type pointer in each object header. - **Class → ClassLoader**: every `Class` records its *defining* classloader; `clazz.getClassLoader()` returns it (or `null` for bootstrap-loaded classes). - **ClassLoader → its Classes**: a classloader keeps a collection of all classes it has defined (so it can satisfy delegation and avoid re-defining). This is the edge that makes the bundle whole. - **Class → static fields**: a class's `static` fields are stored with the `Class`. So any object referenced by a static field is reachable as long as the `Class` is — and the `Class` is reachable as long as the loader is. Put together: `instance → Class → ClassLoader → {all its Classes} → (each Class's statics) → …`. The graph is effectively a strongly-connected component from the GC's point of view. ## Why 'all-or-nothing' The garbage collector works by reachability: starting from GC roots, it marks everything it can reach; the rest is collectible. Suppose a long-lived thread's `ThreadLocal` holds one app object `x`. Then: - `x` is reachable (root → ThreadLocalMap → x). - `x.getClass()` (call it `C`) is reachable. - `C.getClassLoader()` (the app loader `L`) is reachable. - `L`'s collection of all its defined classes is reachable — so *every* class `L` loaded is reachable. - Each of those classes' static fields, and any objects they reference, are reachable. None of `L`'s classes can be unloaded; none of its metaspace can be reclaimed. You cannot 'partially' collect a classloader. This is precisely why a *single* forgotten reference is catastrophic: it doesn't leak proportionally to what you forgot, it leaks the *whole deployment's* class metadata (which in a real web app is thousands of classes plus all their dependencies loaded by that loader). ## Finding the GC-root path The practical corollary: to diagnose, take a **heap dump** (e.g. `jmap -dump:live`, or `-XX:+HeapDumpOnOutOfMemoryError`) and, in a tool like Eclipse MAT, find the leaked `ClassLoader` instance and run **'path to GC roots'** (excluding weak/soft references). That single shortest strong-reference path is the thing to cut — remove a `ThreadLocal`, stop a thread, deregister a driver/listener, or null a static cache entry. ## Why weak references help here If a cache or registry that must outlive the app holds app objects via **weak** references (or weak keys, e.g. `WeakHashMap`), those references are *not* counted as strong reachability — so they don't pin the loader. That's a common mitigation: shared infrastructure references plugin/app objects weakly so it never becomes the GC root that traps a loader. ## Summary The leak's severity comes from the JVM's design that ties an object's lifetime to its class's lifetime to its loader's lifetime. Understanding the four edges lets you both *explain* why one reference is enough and *fix* it by cutting the single strong path the heap dump reveals.

  • How would weak references in a shared cache prevent the loader from being pinned?
    Weak references aren't counted as strong reachability by the GC, so the app object can still be collected even while cached. Once it's gone, the chain to the loader is broken and the loader can be unloaded. WeakHashMap (weak keys) is the canonical tool.
  • In a heap-dump tool, what specific action isolates the leaking reference?
    Find the dead ClassLoader instance, then run 'path to GC roots' excluding weak/soft/phantom references. The remaining strong path is the exact reference to sever.

saying these in an interview costs you the question

  • Saying only the leaked object stays, not the entire loader and its classes
  • Forgetting that static fields keep their referents alive via the Class
  • Believing the GC can unload some of a loader's classes but not others
  • Not knowing how to locate the root path (heap dump / path-to-GC-roots)

context

open as a page

How would you diagnose and confirm a classloader leak in a running JVM?

level: middleimportance: should knowfreq 35%

basics

~20 s

Watch metaspace climb with each redeploy and never fall, ending in OutOfMemoryError: Metaspace. Then take a heap dump, find the classloaders that should be dead, and use the tool's 'path to GC roots' to see which reference is keeping each one alive.

open as a page

What is a classloader leak in Java, and what symptom does it typically produce?

level: middleimportance: should knowfreq 45%

basics

~20 s

It happens when something keeps a reference to a class (or its instance/static field) loaded by a custom classloader, so the JVM can never unload that classloader. Its classes pile up in metaspace, eventually causing OutOfMemoryError: Metaspace.

open as a page

Why are ThreadLocals on pooled threads a notorious cause of classloader leaks, and how do you avoid it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Pooled threads (like a server's thread pool) outlive your app. If a request sets a ThreadLocal to an app object and never removes it, that value stays attached to the long-lived thread, keeping your app's classloader alive. Always remove() ThreadLocals in a finally block.

open as a page

From an architecture standpoint, how do you design plugin/redeploy systems to be resistant to classloader leaks?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Give each plugin/app its own classloader, never let long-lived infrastructure hold strong references to plugin objects (use weak references and explicit deregistration), and clean up threads, ThreadLocals, drivers, and listeners on undeploy. Test by redeploying many times and confirming the old classloader is collected.

open as a page