skip to content

How is class-metadata memory allocated and released inside the JVM, and why can metaspace usage stay high even after an application stops using the classes involved?

level: seniorimportance: should knowfreq 34%

answer

  1. Per-class-loader arena, chunk list, bump allocation
  2. No compaction, no per-class free
  3. Loader unload = the only release event
  4. Class references its loader; instance references its class
  5. JDK 16+ uncommits freed chunks to the OS

basics

~20 s

Metadata is allocated in chunks from a per-class-loader arena. Nothing is freed class by class: an arena is released only when its class loader is unloaded during a collection. So one reachable loader keeps all of its classes' metadata committed.

solid answer

~50 s

Each class loader owns a **metaspace arena** built from chunks carved out of larger committed regions. All metadata for the classes it defines is allocated there, bump-pointer style, and is never moved or compacted. Reclamation granularity is the **class loader**, not the class. During a collection the JVM determines which loaders are unreachable, unloads their classes, and frees their arenas wholesale. There is no path by which an individual unused class returns memory while its loader lives. That is why usage plateaus high: a single lingering reference to a loader (or to any class it defined, since a class strongly references its loader) pins every byte of its metadata. Repeated dynamic loading with retained loaders shows the classic staircase, where committed metaspace steps up and never comes down. Since JDK 16 the allocator was reworked: chunks come from a buddy allocator and freed memory is **uncommitted back to the OS**, so unloading actually shrinks the process footprint rather than leaving it reserved.

go deeper

for a junior

Know that metadata is grouped per class loader and released only when that loader is unloaded, never class by class.

for a middle

Add why loaders stay reachable (instance to class to loader) and that release happens during collection cycles rather than on demand.

for a senior

Distinguish plateau from staircase when reading usage, use per-loader arena reporting to identify accumulating loaders, and know that uncommit behaviour depends on the JDK version.

for a principal

Treat loader lifecycle as an architectural budget: bound the number of loader-creating mechanisms, cap the region so failure is attributable, and size expectations to the JDK's uncommit behaviour.

## Allocation: arenas, chunks, and no compaction Metaspace is not a general-purpose native allocator with per-object `free`. It is organised in three layers. 1. The JVM **reserves** virtual address space and **commits** it in granules as demand grows. 2. Committed space is divided into **chunks** of varying sizes. 3. Each **class loader** owns an **arena**: a list of chunks from which its metadata is bump-allocated. When a loader defines a class, the type structure, method metadata, bytecode and constant pool are all appended into that loader's current chunk; when it fills, another chunk is attached. Nothing is ever relocated, so pointers into metadata stay valid for the life of the arena and no read barrier or compaction phase is needed. A small arena for a loader that defines one class would waste space, so HotSpot hands out small chunks first and larger ones as a loader proves it keeps loading. The boot loader and other long-lived loaders get large chunks immediately. ## Release: only at loader granularity The design consequence is the whole point of the question. Metadata is freed **only when the owning class loader is unloaded**, and a loader can be unloaded only when it and all of its classes are unreachable. Two references make that stricter than people expect: - every `java.lang.Class` object strongly references its defining loader, so holding one class alive holds the loader alive; - every instance references its class, so a single live instance of one class pins the entire loader arena. So there is no such thing as "this class hasn't been used in an hour, free it". Either the whole loader goes or none of it does. During a collection cycle the JVM computes loader reachability, unloads the classes of dead loaders, and returns their chunks. Modern collectors do this concurrently, so class unloading no longer requires a stop-the-world full collection. ## Why usage stays high Given that granularity, high metaspace with an idle application means one of three things: - **Loaders are still reachable.** Dynamic proxies, scripting engines, bytecode-generating frameworks and repeated deployment create a loader per unit; anything retaining one of their classes or instances retains the arena. - **No collection has run recently.** Metadata is only reclaimed as part of a collection cycle, and a JVM with a quiet heap may not run one. Committed metaspace crossing the high-water mark set by `-XX:MetaspaceSize` is itself a trigger, which is why an idle process can suddenly reclaim a large block. - **The classes really are all live.** Large frameworks legitimately load tens of thousands of classes; the plateau is the steady state, not a defect. A useful discriminator is the trend rather than the level: a footprint that rises to a plateau and stays there is normal; a footprint that steps up with each cycle of work and never falls after collections points at retained loaders. ## What changed with the reworked allocator The original JDK 8 implementation suffered two structural weaknesses: chunks freed by unloading were kept for reuse rather than returned to the OS, so the process footprint ratcheted upward, and mismatched chunk sizes fragmented the space so that committed memory exceeded what was actually in use, sometimes substantially. JDK 16 replaced it with an elastic scheme: chunks are managed by a **buddy allocator** that splits and merges neighbouring blocks, allocation commits memory lazily in small granules, and memory freed by class unloading is **uncommitted back to the operating system**. Practically, on JDK 16 and later, unloading a batch of loaders visibly reduces the process's resident memory instead of merely making room for future classes. On JDK 8 through 15, expect the footprint to stay at its peak. ## How to inspect it `jcmd <pid> VM.metaspace` reports used, committed and reserved for the class and non-class parts, and its detailed form lists **arenas per class loader** with each loader's name and class count. That per-loader view is the direct read on the mechanism: many arenas with identical loader names is the signature of loaders accumulating rather than legitimate class-count growth. Enabling class-unloading logging shows which loaders each cycle actually released. ## Answer shape Say: chunk-based per-loader arenas, no compaction, no per-class free, release only on loader unload during a collection; therefore any reference into a loader's classes pins all of its metadata; and since JDK 16 freed metadata is uncommitted to the OS so the footprint can actually shrink.

  • Which references make a class loader reachable, and therefore keep its metadata committed?
    Any live instance of a class it defined, since instances reference their class and every class strongly references its defining loader. Also any reference to a `Class` object, to a static field value held in a class mirror, or to the loader itself from a thread context, cache or registry. Because these edges chain, one retained object pins the loader's entire arena.
  • Why can committed metaspace fall on JDK 17 but not on JDK 8 after the same classes are unloaded?
    JDK 8's allocator retained freed chunks for future metadata rather than returning them, so the process footprint stayed at its high-water mark. JDK 16 introduced an elastic allocator that merges freed blocks and uncommits them to the operating system, so unloading a batch of loaders reduces resident memory.

Each class loader rents a storage unit and fills it with boxes. You cannot remove individual boxes; the unit is emptied only when the tenant's lease ends. One forgotten key held by someone else keeps the whole unit rented.

saying these in an interview costs you the question

  • Believing an individual unused class is freed while its loader lives
  • Assuming metaspace is compacted or defragmented like the heap
  • Saying class unloading always requires a stop-the-world full collection
  • Expecting the process footprint to shrink after unloading on JDK 8
  • Reading a high plateau as a defect without checking whether it grows after each collection

context