skip to content

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

level: middleimportance: should knowfreq 45%

answer

  1. Class unloads only when its classloader is unreachable
  2. instance → Class → ClassLoader → all its classes (all-or-nothing)
  3. Symptom: OutOfMemoryError: Metaspace, ratchets up per redeploy
  4. Hot-redeploy = new loader per version
  5. One dangling reference pins the whole loader

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.

solid answer

~40 s

A classloader leak occurs when a classloader cannot be garbage-collected because something still references it, one of its loaded classes, an instance of such a class, or a static field on it. In the JVM a class is only unloadable when its defining classloader is unreachable, and a classloader is only unreachable when none of its classes are reachable. So a single dangling reference into the old classloader pins all of its classes in metaspace. It surfaces mainly in app servers and frameworks that hot-redeploy: each redeploy creates a fresh classloader for the new version, but if the old one cannot be collected, its metaspace footprint stays. After several redeploys you get 'OutOfMemoryError: Metaspace'. The fix is to find and break the lingering reference into the old classloader's object graph.

go deeper

for a junior

Knows classes live in metaspace and that a leak there can cause OutOfMemoryError; may not know the classloader-reachability mechanism.

for a middle

Can state the unloading rule (class unloads only when its classloader is unreachable) and recognize the per-redeploy metaspace ratchet symptom.

for a senior

Explains the instance→Class→ClassLoader chain, why it's all-or-nothing, and names the common culprits (ThreadLocal, static caches, drivers, threads).

for a principal

Connects it to the loader-as-namespace identity model, reasons about container redeploy lifecycles, and can architect leak-resistant module/plugin loading.

## First, the vocabulary - **Class**: a blueprint for objects, loaded into the JVM from bytecode (a `.class` file). Loading produces a runtime `Class` object plus metadata. - **Classloader**: the object responsible for finding and loading classes. Every loaded class records which classloader *defined* it. Classloaders form a parent-delegation hierarchy: the bootstrap loader (core JDK), the platform/extension loader, the application (system) loader, and any **custom** classloaders an app creates (e.g. a web app's `WebAppClassLoader`). - **Metaspace**: the native (off-heap) memory region, since Java 8, where the JVM stores **class metadata** (the loaded class structures, method bytecode, constant pools, etc.). Before Java 8 this lived in the heap's *PermGen*. Metaspace grows as more classes are loaded and (in principle) shrinks when classes are *unloaded*. - **Reachable / GC root**: an object is *reachable* if you can follow references to it starting from a GC root (a live thread's stack, a static field of a loaded class, a JNI reference, etc.). The garbage collector only reclaims **unreachable** objects. ## The class-unloading rule (the heart of the topic) The JVM may unload a class — reclaim its metaspace — **only when its defining classloader can itself be garbage-collected**. And here is the key coupling: > A classloader is reachable if **any** of these is reachable: (1) the classloader object itself, (2) **any `Class` object it defined**, (3) **any instance** of a class it defined, or (4) **any static field** of a class it defined (statics are held by the `Class`, which is held by the loader). This is because every object carries a reference to its `Class` (via `getClass()`), every `Class` carries a reference to its defining classloader (via `getClassLoader()`), and every classloader keeps references to all the classes it has loaded. So the graph is: `instance → Class → ClassLoader → all its Classes`. The whole bundle is **all-or-nothing**: one live reference into any part of it keeps the *entire* classloader and *all* its classes alive in metaspace. ## Why a 'leak' arises — the hot-redeploy scenario Application servers (Tomcat, JBoss/WildFly, Jetty, OSGi containers) and dev-time reload tools give each deployed application its **own** classloader. To redeploy version N+1 without restarting the JVM, the server: 1. stops the old app, 2. throws away the old classloader and creates a **fresh** one, 3. loads the new classes into the fresh loader. For the old version's metaspace to be reclaimed, the **old classloader must become unreachable**. Usually it does. But if something *outside* the app — something that outlives the redeploy — still holds a reference into the old classloader's graph, the old loader (and every class it loaded) is pinned forever. Repeat the redeploy a few times and metaspace fills with stacked generations of dead classes → **`java.lang.OutOfMemoryError: Metaspace`**. ## Classic culprits (references that escape the app classloader) - **`ThreadLocal`s on pooled threads**: a server's thread pool outlives the app. A `ThreadLocal` value that is an app-loaded object (or whose *class* was app-loaded) keeps the app classloader alive until that pooled thread dies or the value is removed. - **Static caches / singletons in shared (parent-loaded) libraries** that store app objects. - **JDBC drivers** registered in `java.sql.DriverManager` (a bootstrap-loaded class) but defined by the app classloader. - **Background threads** the app started but never stopped: a running thread is a GC root and its stack references app classes. - **Listeners / callbacks / shutdown hooks** registered with JVM-wide or shared registries and never deregistered. - **JVM-internal caches** that cache reflection/JDK objects referencing app classes. ## The symptom and the distinction The tell-tale sign is metaspace usage that **ratchets up with each redeploy and never comes back down**, ending in `OutOfMemoryError: Metaspace` (not 'Java heap space'). A heap-space OOM points at live *data objects*; a metaspace OOM points at *too many loaded classes*, which for a long-running server almost always means classloaders that won't unload. ## How it's a Java-specific, platform-rooted issue This builds directly on the JVM's *loader-as-namespace* model: a class's identity is the pair (fully-qualified name, defining classloader), so the *same* class name loaded by two loaders is two distinct types. That's exactly why redeploy uses a new loader — and exactly why leaks are subtle: a single forgotten reference defeats the whole mechanism.

  • Why does referencing just one app object keep the whole classloader alive?
    Every object references its Class, every Class references its defining classloader, and the classloader references all classes it loaded. So a single reachable instance transitively makes the loader and all its classes reachable — the GC can't unload any of them.
  • Why does this show up after redeploys rather than at startup?
    Each redeploy creates a new classloader for the new version. The old loader's metaspace is only reclaimed if the old loader becomes unreachable; a leak prevents that, so dead generations of classes accumulate over successive redeploys.

saying these in an interview costs you the question

  • Saying it causes 'OutOfMemoryError: Java heap space' — it's the Metaspace (or pre-8 PermGen) variant
  • Claiming classes are never unloaded in Java — they are, once their classloader is collectible
  • Thinking only the leaked object stays, not the entire classloader and all its classes
  • Confusing it with an ordinary heap leak of data objects

context