JVM Class Loading
Purpose
Reason about class identity and classloader lifetime. Two failures live here and both
look like something else: a ClassCastException where the two type names are identical,
and a Metaspace that grows forever while every heap dashboard looks healthy.
For ordinary named classes, runtime identity includes the binary name and defining loader;
an initiating loader may merely delegate to that definition. Ordinary classes normally become
unloadable with their defining loader. Weak hidden classes are the deliberate exception: unless
defined with STRONG, they may unload while their marked defining loader remains reachable.
Inspect the Java toolchain, JVM vendor/build, launch/module paths and loader implementation
before applying commands or version-sensitive advice. Examples use Java 17-compatible partial
snippets; diagnostic output is scoped to Temurin 25.0.3 and AOT features state their own minimum
release. This does not authorize an upgrade or a global access override.
Workflow
- On a confusing
ClassCastException, print the loaders of both sides first, before
any other hypothesis. Capture each Class object's defining loader, module, binary name and
code source; identical names from different definitions are incompatible types.
- Classify lookup, linkage and initialization separately.
ClassNotFoundException is the
checked result of name-based loading APIs that cannot find a definition. JVM loading or
resolution may wrap an underlying loader failure as NoClassDefFoundError; the same error
class also reports a definition whose <clinit> previously failed. Preserve the earliest
exception, complete cause chain, failing instruction and loader identities—message text alone
is not a complete taxonomy.
- Check whether it is a module problem instead.
IllegalAccessError mentioning
"does not export" points to ordinary access; InaccessibleObjectException mentioning
does not "opens" points to deep reflection. Prefer fixing the API, dependency or owned
module descriptor; use narrow --add-exports/--add-opens only when justified.
An export alone does not permit access to private members. See references/module-access.md.
- For suspected leaks, establish a cohort and unloading opportunity: capture
jcmd <pid> VM.classloader_stats, exercise N equivalent reload/redeploy cycles, allow the
configured collector to perform class unloading, then capture again. Persistent growth in
obsolete loader cohorts is evidence of retention; raw loaded-class growth alone is not proof.
- Find the retainer with
jcmd <pid> GC.heap_dump and Path to GC Roots in Eclipse
MAT, excluding weak references. See references/classloader-leaks.md.
- Validate the fix by repeating the same first measurement, not by absence of
symptoms.
- For loader-constraint or duplicate-definition failures, reconstruct the graph: the
initiating loader at each symbolic reference, the eventual defining loader, delegation order,
duplicate class/resources and the shared method descriptor.
LinkageError: loader constraint violation means two namespaces were forced to agree on a descriptor type and did not; adding
casts or changing load order is not a fix.
Rules
close() on a URLClassLoader releases the JARs, not Metaspace. A reachable instance of an ordinary class
defined by that loader retains its class/loader; a parent-defined value created by plugin
code does not necessarily retain the plugin. Weak hidden-class exceptions still apply. Confusing these two is the most common cause of "I close the loader and
Metaspace keeps growing".
- Parent-first delegation preserves namespace consistency and helps prevent child artifacts from
shadowing platform/shared API classes. Child-first isolation requires an explicit boundary:
always delegate platform namespaces and shared contract types, define package/resource order,
and test split-package, service-provider and sealing behavior.
- Custom loaders are not parallel-capable by default. For the standard
loadClass implementation, absent successful registerAsParallelCapable() registration,
getClassLoadingLock uses the whole loader rather than a per-name lock. Overrides can
change synchronization and must establish their own correctness. Registration also depends on the superclass chain;
check the boolean result/isRegisteredAsParallelCapable() and keep loadClass idempotent under
concurrent requests for the same name.
Class.forName(name) initializes through the caller's defining loader. Use that for
library-owned types; use a loader explicitly supplied by the plugin/container contract for
isolated code. Use the thread context class loader only for APIs whose provider-discovery
contract requires it, scope any temporary change with try/finally, and avoid retaining it on
long-lived pooled threads.
- Keep
<clinit> trivial. The initializing thread marks initialization in progress and releases
the protocol lock before executing initializers. Other threads whose active use requires
completion still wait for that initialization—and this enables initialization deadlock. If two threads each begin one of two mutually dependent initializers,
they can wait for each other permanently; the tell in jcmd <pid> Thread.print is - waiting on the Class initialization monitor for X under a thread
reported as RUNNABLE, so a deadlock detector that looks only at monitors and locks
reports nothing. See references/class-initialisation.md.
<clinit> re-entered by the same thread does not block: JVMS 5.5 returns at once and
the code observes static fields not yet assigned — null, 0 — while compile-time
constants read as initialised because javac inlined them. A static singleton whose
constructor reads a later static field is the usual shape.
- A
public static final compile-time constant is copied into clients' class files. Changing it
without recompiling consumers can leave old values in the same process, and reading it does not
initialize the declaring class. Do not use mutable operational values as constant variables.
- Loading is not initializing. CDS/AOT can reuse selected metadata, linked state and constrained
runtime objects; do not infer that arbitrary application
<clinit> ran or was skipped. Measure
class loading separately from initialization and framework/application work.
- A custom classloader is the wrong tool for reloading configuration. It brings type
isolation you did not ask for and leak risk you do not need — reload a config object
instead, and reserve loaders for isolated code.
- Every reloadable component needs a symmetric stop protocol: cancel/join its threads, close
executors/resources, deregister JDBC drivers/MBeans/listeners/providers, clear TCCLs and remove
parent-owned cache entries keyed by its
Class objects. Moving an implementation to a shared
loader trades unloadability for process-wide version coupling; share stable contracts, not all
self-registering implementations by default.
- Class loaders and module layers are namespace/access mechanisms, not a sandbox for hostile code.
Code defined into the process can consume CPU/memory, call available native/process APIs and
exploit granted capabilities; isolate untrusted plugins at an OS/process boundary.
- A native library is associated with a class loader namespace and may refuse a second load from
another loader. Plugin reload designs that use JNI must own
JNI_OnUnload, native threads and
callbacks explicitly; Java reachability alone cannot prove native state was released.
Unsafe::defineAnonymousClass was removed in JDK 17. Lambdas and generated bytecode use
hidden classes (JEP 371). A default weak hidden class may unload independently when its
Class and instances are unreachable; STRONG ties unloading to the defining loader. Current
lambda proxy implementation details must be measured for the deployed JDK, and every live
generated class still consumes metadata.
Selection framework
| Need |
Prefer |
Avoid or constrain |
| Load an application/library-owned type |
Caller/defining loader |
Ambient TCCL guessing |
| Discover providers in a container |
Contract-selected loader or scoped TCCL |
Leaving TCCL changed on pooled threads |
| Isolate reloadable code |
Module layer or explicit child loader with parent-shared API |
Duplicating API types across loaders |
| Reload configuration/data |
Replace immutable state through an application lifecycle |
New loader per refresh |
| Generate many short-lived implementation classes |
Weak hidden classes when name discovery/redefinition is unnecessary |
STRONG without a lifetime reason |
Before accepting a custom loading architecture, specify delegation for classes and resources,
shared API ownership, package sealing/signers, module readability/exports/opens, lifecycle cleanup,
parallel-capable locking, observability, and the security provenance of bytes passed to
defineClass.
Production evidence packet
Collect before restarting or flattening the class path:
jcmd <pid> VM.classloaders verbose=true
jcmd <pid> VM.classloader_stats
jcmd <pid> Thread.print
Add a bounded -Xlog:class+load=info,class+unload=info reproduction when safe; add
class+loader+constraints=info for a loader-constraint failure. For both sides of an
identity/access failure record type.getName(), type.getClassLoader(), type.getModule() and
type.getProtectionDomain().getCodeSource() (the latter can be null). Redact paths if they
expose tenant/build information. Do not infer origin from a class name or JAR filename alone.
The remediation must pass concurrent first-load, duplicate artifact, missing optional provider,
reload/unload, shutdown, module-boundary and supported-JDK tests. Plugin tests must assert that
objects crossing the boundary implement parent-owned contracts and that no plugin thread/TCCL or
registration survives stop.
References
- Classloader leaks — the confirmation procedure, the
usual retainers, and the validation step. Read when Metaspace or loader count grows
across redeploys or plugin reloads.
- Class initialisation — the JVMS 5.5 procedure as it
matters in practice, the deadlock and recursion reproductions with the thread-dump
signature, the
NoClassDefFoundError cause chain, and -Xlog:class+init. Read when a
startup hangs, when a static field is unexpectedly null, or when the same
NoClassDefFoundError repeats after a first, different exception.
- Module access — static versus reflective access across
module boundaries,
--add-exports versus --add-opens, where the flags can be placed
(command line, JDK_JAVA_OPTIONS, the Add-Opens manifest attribute and its -jar-only
scope), and how the module system changes loader delegation. Read when an
IllegalAccessError or InaccessibleObjectException names a module.
- Startup: CDS and the AOT cache — what JEP
483/514/515 actually cache, how the cache is invalidated, and how to verify it is being
used. Read when reducing cold start.
1---2name: jvm-class-loading3description: Class loading, class identity and classloader leaks: parent-first delegation, {defining loader, binary name} identity, loading versus linking versus initialisation, Metaspace retention, and CDS/AOT cache for startup. Use when a ClassCastException reports identical type names on both sides, when Metaspace grows monotonically across redeploys or plugin reloads, when ClassNotFoundException and NoClassDefFoundError need to be told apart, when IllegalAccessError mentions "does not export" or InaccessibleObjectException asks for --add-opens, when a startup hangs with "waiting on the Class initialization monitor" in a thread dump, when a static initialiser does I/O, or when reducing cold start. Does not cover the Metaspace budget itself (jvm-memory-regions), JIT warm-up (jit-compilation), or heap object-retention analysis (heap-dump-analysis). Metaspace internals are metaspace-internals and startup caching in depth is startup-cds-crac-leyden.4---56# JVM Class Loading78## Purpose910Reason about class identity and classloader lifetime. Two failures live here and both11look like something else: a `ClassCastException` where the two type names are identical,12and a Metaspace that grows forever while every heap dashboard looks healthy.1314For ordinary named classes, runtime identity includes the binary name and defining loader;15an initiating loader may merely delegate to that definition. Ordinary classes normally become16unloadable with their defining loader. Weak hidden classes are the deliberate exception: unless17defined with `STRONG`, they may unload while their marked defining loader remains reachable.1819Inspect the Java toolchain, JVM vendor/build, launch/module paths and loader implementation20before applying commands or version-sensitive advice. Examples use Java 17-compatible partial21snippets; diagnostic output is scoped to Temurin 25.0.3 and AOT features state their own minimum22release. This does not authorize an upgrade or a global access override.2324## Workflow25261. **On a confusing `ClassCastException`, print the loaders of both sides first**, before27 any other hypothesis. Capture each `Class` object's defining loader, module, binary name and28 code source; identical names from different definitions are incompatible types.292. **Classify lookup, linkage and initialization separately.** `ClassNotFoundException` is the30 checked result of name-based loading APIs that cannot find a definition. JVM loading or31 resolution may wrap an underlying loader failure as `NoClassDefFoundError`; the same error32 class also reports a definition whose `<clinit>` previously failed. Preserve the earliest33 exception, complete cause chain, failing instruction and loader identities—message text alone34 is not a complete taxonomy.353. **Check whether it is a module problem instead.** `IllegalAccessError` mentioning36 "does not export" points to ordinary access; `InaccessibleObjectException` mentioning37 `does not "opens"` points to deep reflection. Prefer fixing the API, dependency or owned38 module descriptor; use narrow `--add-exports`/`--add-opens` only when justified.39 An export alone does not permit access to private members. See `references/module-access.md`.404. **For suspected leaks, establish a cohort and unloading opportunity:** capture41 `jcmd <pid> VM.classloader_stats`, exercise N equivalent reload/redeploy cycles, allow the42 configured collector to perform class unloading, then capture again. Persistent growth in43 obsolete loader cohorts is evidence of retention; raw loaded-class growth alone is not proof.445. **Find the retainer** with `jcmd <pid> GC.heap_dump` and _Path to GC Roots_ in Eclipse45 MAT, excluding weak references. See `references/classloader-leaks.md`.466. **Validate the fix by repeating the same first measurement**, not by absence of47 symptoms.487. **For loader-constraint or duplicate-definition failures, reconstruct the graph:** the49 initiating loader at each symbolic reference, the eventual defining loader, delegation order,50 duplicate class/resources and the shared method descriptor. `LinkageError: loader constraint51violation` means two namespaces were forced to agree on a descriptor type and did not; adding52 casts or changing load order is not a fix.5354## Rules5556- `close()` on a `URLClassLoader` releases the JARs, **not** Metaspace. A reachable instance of an ordinary class57 defined by that loader retains its class/loader; a parent-defined value created by plugin58 code does not necessarily retain the plugin. Weak hidden-class exceptions still apply. Confusing these two is the most common cause of "I close the loader and59 Metaspace keeps growing".60- Parent-first delegation preserves namespace consistency and helps prevent child artifacts from61 shadowing platform/shared API classes. Child-first isolation requires an explicit boundary:62 always delegate platform namespaces and shared contract types, define package/resource order,63 and test split-package, service-provider and sealing behavior.64- Custom loaders are not parallel-capable by default. For the standard65 `loadClass` implementation, absent successful `registerAsParallelCapable()` registration,66 `getClassLoadingLock` uses the whole loader rather than a per-name lock. Overrides can67 change synchronization and must establish their own correctness. Registration also depends on the superclass chain;68 check the boolean result/`isRegisteredAsParallelCapable()` and keep `loadClass` idempotent under69 concurrent requests for the same name.70- `Class.forName(name)` initializes through the caller's defining loader. Use that for71 library-owned types; use a loader explicitly supplied by the plugin/container contract for72 isolated code. Use the thread context class loader only for APIs whose provider-discovery73 contract requires it, scope any temporary change with `try/finally`, and avoid retaining it on74 long-lived pooled threads.75- Keep `<clinit>` trivial. The initializing thread marks initialization in progress and releases76 the protocol lock before executing initializers. Other threads whose active use requires77 completion still wait for that initialization—and this enables initialization deadlock. If two threads each begin one of two mutually dependent initializers,78 they can wait for each other permanently; the tell in `jcmd <pid>79Thread.print` is `- waiting on the Class initialization monitor for X` under a thread80 reported as `RUNNABLE`, so a deadlock detector that looks only at monitors and locks81 reports nothing. See `references/class-initialisation.md`.82- `<clinit>` re-entered by the **same** thread does not block: JVMS 5.5 returns at once and83 the code observes `static` fields not yet assigned — `null`, `0` — while compile-time84 constants read as initialised because `javac` inlined them. A static singleton whose85 constructor reads a later static field is the usual shape.86- A `public static final` compile-time constant is copied into clients' class files. Changing it87 without recompiling consumers can leave old values in the same process, and reading it does not88 initialize the declaring class. Do not use mutable operational values as constant variables.89- Loading is not initializing. CDS/AOT can reuse selected metadata, linked state and constrained90 runtime objects; do not infer that arbitrary application `<clinit>` ran or was skipped. Measure91 class loading separately from initialization and framework/application work.92- A custom classloader is the wrong tool for reloading _configuration_. It brings type93 isolation you did not ask for and leak risk you do not need — reload a config object94 instead, and reserve loaders for isolated **code**.95- Every reloadable component needs a symmetric stop protocol: cancel/join its threads, close96 executors/resources, deregister JDBC drivers/MBeans/listeners/providers, clear TCCLs and remove97 parent-owned cache entries keyed by its `Class` objects. Moving an implementation to a shared98 loader trades unloadability for process-wide version coupling; share stable contracts, not all99 self-registering implementations by default.100- Class loaders and module layers are namespace/access mechanisms, not a sandbox for hostile code.101 Code defined into the process can consume CPU/memory, call available native/process APIs and102 exploit granted capabilities; isolate untrusted plugins at an OS/process boundary.103- A native library is associated with a class loader namespace and may refuse a second load from104 another loader. Plugin reload designs that use JNI must own `JNI_OnUnload`, native threads and105 callbacks explicitly; Java reachability alone cannot prove native state was released.106- `Unsafe::defineAnonymousClass` was removed in JDK 17. Lambdas and generated bytecode use107 hidden classes (JEP 371). A default weak hidden class may unload independently when its108 `Class` and instances are unreachable; `STRONG` ties unloading to the defining loader. Current109 lambda proxy implementation details must be measured for the deployed JDK, and every live110 generated class still consumes metadata.111112## Selection framework113114| Need | Prefer | Avoid or constrain |115| ------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------- |116| Load an application/library-owned type | Caller/defining loader | Ambient TCCL guessing |117| Discover providers in a container | Contract-selected loader or scoped TCCL | Leaving TCCL changed on pooled threads |118| Isolate reloadable code | Module layer or explicit child loader with parent-shared API | Duplicating API types across loaders |119| Reload configuration/data | Replace immutable state through an application lifecycle | New loader per refresh |120| Generate many short-lived implementation classes | Weak hidden classes when name discovery/redefinition is unnecessary | `STRONG` without a lifetime reason |121122Before accepting a custom loading architecture, specify delegation for classes **and resources**,123shared API ownership, package sealing/signers, module readability/exports/opens, lifecycle cleanup,124parallel-capable locking, observability, and the security provenance of bytes passed to125`defineClass`.126127## Production evidence packet128129Collect before restarting or flattening the class path:130131```bash132jcmd <pid> VM.classloaders verbose=true133jcmd <pid> VM.classloader_stats134jcmd <pid> Thread.print135```136137Add a bounded `-Xlog:class+load=info,class+unload=info` reproduction when safe; add138`class+loader+constraints=info` for a loader-constraint failure. For both sides of an139identity/access failure record `type.getName()`, `type.getClassLoader()`, `type.getModule()` and140`type.getProtectionDomain().getCodeSource()` (the latter can be null). Redact paths if they141expose tenant/build information. Do not infer origin from a class name or JAR filename alone.142143The remediation must pass concurrent first-load, duplicate artifact, missing optional provider,144reload/unload, shutdown, module-boundary and supported-JDK tests. Plugin tests must assert that145objects crossing the boundary implement parent-owned contracts and that no plugin thread/TCCL or146registration survives stop.147148## References149150- [Classloader leaks](references/classloader-leaks.md) — the confirmation procedure, the151 usual retainers, and the validation step. Read when Metaspace or loader count grows152 across redeploys or plugin reloads.153- [Class initialisation](references/class-initialisation.md) — the JVMS 5.5 procedure as it154 matters in practice, the deadlock and recursion reproductions with the thread-dump155 signature, the `NoClassDefFoundError` cause chain, and `-Xlog:class+init`. Read when a156 startup hangs, when a static field is unexpectedly `null`, or when the same157 `NoClassDefFoundError` repeats after a first, different exception.158- [Module access](references/module-access.md) — static versus reflective access across159 module boundaries, `--add-exports` versus `--add-opens`, where the flags can be placed160 (command line, `JDK_JAVA_OPTIONS`, the `Add-Opens` manifest attribute and its `-jar`-only161 scope), and how the module system changes loader delegation. Read when an162 `IllegalAccessError` or `InaccessibleObjectException` names a module.163- [Startup: CDS and the AOT cache](references/startup-and-aot-cache.md) — what JEP164 483/514/515 actually cache, how the cache is invalidated, and how to verify it is being165 used. Read when reducing cold start.