Skip to content
Back to skills

Jvm Class Loading

ASecurity

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 Inacc...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
developmentrustjavabashapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add robsonkades/agent-skills --skill jvm-class-loading --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Jvm Class Loading?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Jvm Class Loading
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robsonkades-jvm-class-loading/badge)](https://www.skillsdirectory.com/skills/robsonkades-jvm-class-loading)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: jvm-class-loading
description: >
  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.
---

# 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

Choose the branch that answers the request and reuse adequate source, launch, stack or heap
evidence already supplied. A narrow explanation needs the relevant contract and counterexample;
it does not require a production capture or a plugin test matrix. When evidence cannot separate
the hypotheses, state the gap and the smallest useful next check. Keep a sound existing design
when the evidence does not justify a change.

1. **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.
2. **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.
3. **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`.
4. **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.
5. **For retention that still needs attribution, find the strong path to a GC root.** Reuse
   an adequate dump and _Path to GC Roots_ in Eclipse MAT, excluding weak references; capture
   with `jcmd <pid> GC.heap_dump <file>` only if the missing evidence warrants its pause, disk
   and data exposure. See `references/classloader-leaks.md` before capture.
6. **Validate a change against the failure it addresses.** Repeat comparable lifecycle
   measurements for a leak fix; use a focused reproduction for identity, access or initialization.
7. **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

- A class loading successfully does not prove binary compatibility. For `NoSuchMethodError` or
  `NoSuchFieldError`, compare the failing caller's symbolic owner, member name and full descriptor
  with the definition actually selected by its loader. A method descriptor includes its return
  type: a same-named method with the same parameter list can still be incompatible. Inspect
  caller/provider bytes (`javap -c -p -s -v` on the relevant class files) and the deployed dependency
  graph, not just source or the build-time classpath; account for transformed/generated bytes.
  Restore compatible artifacts or the shared API contract. Adding `opens` or retrying an
  initializer cannot supply a missing member. For descriptor decoding, verifier or class-file
  version failures, pass the bytes, failing instruction and JVM build to `jvm-bytecode`; without
  those bytes, keep artifact mismatch as a hypothesis and request the smallest missing evidence.
  See [JVMS 17 member resolution](https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-5.html#jvms-5.4.3).
- `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. A `null` parent selects bootstrap
  visibility, not all platform classes. On Java 17+, use `ClassLoader.getPlatformClassLoader()`
  when the boundary requires platform APIs but not application classes; use the shared API's
  defining loader when that contract must cross the boundary. Check custom runtime images for
  missing modules before attributing every platform lookup failure to parent selection. See the
  [ClassLoader parent contract](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ClassLoader.html).
- 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 can observe default values of non-constant `static` fields not yet assigned —
  `null`, `0`. Constant variables are initialized before ordinary initializers; source-level
  inlining is a separate reason their reads can avoid a field access. A static singleton whose
  constructor reads a later non-constant static field is the usual hazard.
- 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 ordinary compiled
  reads do not initialize the declaring class. Reflective field access has its own initialization
  trigger. 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. Stop admission and drain in-flight plugin
  calls before closing a `URLClassLoader`; concurrent class loading during `close()` is undefined.
  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. Code generators can use ordinary named
  classes or hidden classes (JEP 371); generated bytecode is not automatically weak or hidden.
  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

For an unresolved production incident, preserve relevant evidence before restarting or changing
the class path. Select the commands needed for the symptom and check support on the target JVM:

```bash
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.

For a custom-loader or plugin-lifecycle change, select the affected checks: concurrent first-load,
duplicate artifacts, optional-provider failure, reload/unload, shutdown and module boundaries on
supported JDKs. Assert parent-owned contracts for objects crossing a shared API boundary; for
reload changes, assert that plugin-owned workers terminate and stale TCCLs/registrations are
removed. An explanation or isolated access fix does not require every plugin scenario.

Return the relevant loader/module/initialization facts, the supported diagnosis or remaining
hypothesis, the justified change or no-change decision, and what validation actually established.
Name unresolved evidence only where it changes the conclusion; scale the answer to the request.

## References

- [Classloader leaks](references/classloader-leaks.md) — the confirmation procedure, the
  usual retainers, and the validation step. Read when Metaspace or loader count grows
  across redeploys or plugin reloads.
- [Class initialisation](references/class-initialisation.md) — 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](references/module-access.md) — 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), resource encapsulation, and how the module system changes loader delegation. Read when
  an `IllegalAccessError` or `InaccessibleObjectException` names a module, or a packaged resource
  is missing even though its class loads.
- [Startup: CDS and the AOT cache](references/startup-and-aot-cache.md) — 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.

Files in this skill

  • SKILL.md14.1 KB
  • references/class-initialisation.md8.7 KB
  • references/classloader-leaks.md5 KB
  • references/module-access.md8.6 KB
  • references/startup-and-aot-cache.md5.2 KB
  • skill.yaml2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…