Metaspace Internals
Purpose
Decide which ceiling a metaspace problem is actually hitting, and whether the fix is a
number or a code change. Metaspace has an overall commitment boundary and, when compressed
class pointers are used, a separately reserved class-space boundary; they interact rather
than form two perfectly independent pools. A heap dashboard alone does not distinguish them.
Inspect effective constraints: changing MaxMetaspaceSize can also change class-space reservation
at startup, while a running exhausted class-space reservation cannot expand beyond its limit.
On the verified 64-bit JDK 25 build, MaxMetaspaceSize defaults to SIZE_MAX. In a
container, metadata growth can therefore compete with the whole cgroup before a configured
fail-fast cap is reached; depending on allocation and kernel policy, either a JVM Metaspace
OOM or an external OOM kill may occur. Do not infer one outcome from the missing flag.
Inspect the target toolchain, JDK build, collector, compressed-pointer mode and deployment
before applying the JDK 25 observations below. JDK 16 introduced Elastic Metaspace, but flags,
allocation granularity and diagnostic layouts vary. This skill does not authorize an upgrade.
Workflow
- Read the exception text before touching a flag.
OutOfMemoryError: Metaspace and
OutOfMemoryError: Compressed class space identify different failed allocation domains.
Inspect both effective constraints and startup ergonomics; increasing an overall cap is
not a general repair for an independently exhausted class-space reservation.
- Compare all memory domains. Normal heap occupancy does not identify metaspace as
the cause of RSS growth/OOMKilled; abnormal heap and metadata retention can coexist,
including Java objects retaining loaders. Correlate cgroup, residency and metadata evidence.
- Take a time series, not a sample. Run low-impact
VM.metaspace basic deliberately,
use periodic class-loading statistics, and interpret jdk.MetaspaceSummary at the GC
boundaries where it is emitted. Growth in used/committed/classes/loaders plus unload/
arena-death behavior is the signal; committed alone can reflect policy/fragmentation.
- Split non-class from class space.
VM.metaspace reports Non-Class, Class and
Both separately. Interpret used, committed, free chunks and waste together; a rising
waste percentage indicates allocation/chunk inefficiency, not a deterministic prediction
of which boundary fails first.
- Decide lifecycle versus capacity. Normalize load, distinct generated inputs,
redeploys and warm-up. Plateau does not prove correct sizing; continued growth may be
legitimate cardinality or delayed unloading. Loader/CLD reachability and generator cache
keys decide whether it is defective (
jvm-class-loading).
- Size from distributions and failure policy. Cover startup/peak/redeploy/generation
regimes, class/non-class growth, fragmentation and correlated native peaks. Choose a cap
that fails before the cgroup only when that fail-fast behavior is desirable; no universal
committed × 1.5 margin exists.
- Classify runtime generation before changing it. Unbounded retained generation needs
lifecycle/cardinality control; a legitimate bounded class population may instead need
capacity. Record raising a ceiling against unresolved growth as mitigation.
Rules
- On the verified 64-bit JDK 25 build,
MaxMetaspaceSize prints SIZE_MAX. Set it only as
a derived fail-fast/capacity boundary: too low creates avoidable OOM, and it cannot
guarantee beating an external cgroup kill caused by another domain or transient overlap.
- The compressed class-space reservation is controlled by
CompressedClassSpaceSize; 1073741824 bytes is the verified JDK 25 default, not a
universal fixed ceiling. Klass metadata lives there; method metadata, constant pools and annotations
live in the non-class space.
UseCompressedClassPointers is independent of UseCompressedOops. Above roughly
32 GB of heap UseCompressedOops turns itself off ergonomically while
UseCompressedClassPointers can stay enabled. Actual class-space reservation depends on
effective flags, alignment and MaxMetaspaceSize ergonomics; 1 GB is not universal. Check
flag availability on the exact release rather than using deprecation history as a runtime test.
-XX:MetaspaceExpansionSize does not exist. The real flags are
-XX:MinMetaspaceExpansion (327680 bytes) and -XX:MaxMetaspaceExpansion
(5439488 bytes). -XX:MetaspaceSize (22020096 bytes) is the threshold that triggers
the first metaspace-driven collection, not a size limit.
- Class metadata becomes reclaimable as its CLD unloads; freed chunks can be reused and
eligible granules can be uncommitted according to Elastic Metaspace policy. Since JEP 387
(JDK 16+) that return is finer-grained; before it,
large blocks stayed committed. Do not quote pre-16 behaviour for a JDK 17, 21 or 25
baseline.
System.gc() does not release a ClassLoader that is still strongly reachable. Remove
retainers, then verify unloading with the selected collector, flags and collection opportunities;
reachability changes do not promise immediate collection.
- Distinguish
reserved, NMT/metaspace committed, used, process-resident and cgroup-
charged in every reading. Committed is not identical to RSS or memory.current; reconcile
timestamps instead of treating it as the bytes the OOM killer sees.
jstat -gcmetacapacity reports MC and CCSC (the column is CCSC, not CCS) as
capacity, not usage, and its counters update on internal GC accounting events — a
freshly started process can report MC = 0.0 while VM.metaspace already shows
committed memory on some builds. Cross-check VM.metaspace; basic output reads live
counters but is not a guaranteed atomic snapshot of concurrent activity. MC includes class
and non-class committed space on JDK 25; CCSC is its class-space subset.
- Every non-strong hidden class is its own
ClassLoaderData with its own chunks —
3 KB committed for the smallest one on 25.0.3 (VM.metaspace show-loaders). Growth from
runtime generation is classified by the generator's cache key and loader lifetime.
Lambdas/proxies are commonly code-keyed and plateau; scripts, expressions and per-instance
proxies can be data-keyed and grow with distinct inputs. Verify the implementation cache.
- CDS and AppCDS can reduce newly allocated metadata when eligible classes are shared; mapped archives appear
under
Shared class space in VM.native_memory, not as newly committed metaspace.
- None of this applies to a GraalVM
native-image binary, where classes are frozen at
build time under its own constraints. HotSpot with a Graal JIT still uses HotSpot metaspace;
verify build-specific flags and account separately for compiler allocations.
References
- Reading metaspace from a live JVM — the
jcmd
commands, the real nested VM.native_memory layout, and the JFR events confirmed
against jfr metadata on JDK 25. Read before capturing evidence from a running
process, or when a tool's output does not look like what you expected.
- Flags, defaults and the sizing protocol — measured
OpenJDK 25 defaults for every metaspace flag and the step-by-step sizing and
validation procedure. Read when choosing a value for
MaxMetaspaceSize or
CompressedClassSpaceSize, or when validating that a change worked.
- Runtime class generation — what a generated
class costs, which generators are bounded by code and which grow with data (lambdas,
proxies, method handles, mocks, scripting and expression engines), the naming patterns
that attribute them in
show-loaders, and the fix per finding. Read when metaspace grows
in a process that never redeploys, or when classloader_stats shows many one-class
loaders or + hidden classes rows.
1---2name: metaspace-internals3description: Metaspace internals on JDK 16+: chunk and arena allocation per ClassLoaderData, the compressed class space and its separately configured reservation/limit, chunk waste and fragmentation, when memory is actually returned to the OS, and reading `jcmd VM.metaspace` and the nested `VM.native_memory` output. Use when `OutOfMemoryError: Metaspace` or `Compressed class space` is thrown, when a container is OOMKilled with a healthy heap, when metaspace committed grows monotonically, when `MaxMetaspaceSize` is unset or copied from another service, when `waste` in the class space is climbing, or when proxies, hidden classes or a scripting engine generate classes at runtime. Does not cover the process-wide memory map and container budget (jvm-memory-regions), classloader identity, unloading and the retainer hunt for a leak (jvm-class-loading), or anything about compiled code and the code cache (code-cache-segments).4---56# Metaspace Internals78## Purpose910Decide which ceiling a metaspace problem is actually hitting, and whether the fix is a11number or a code change. Metaspace has an overall commitment boundary and, when compressed12class pointers are used, a separately reserved class-space boundary; they interact rather13than form two perfectly independent pools. A heap dashboard alone does not distinguish them.14Inspect effective constraints: changing MaxMetaspaceSize can also change class-space reservation15at startup, while a running exhausted class-space reservation cannot expand beyond its limit.1617On the verified 64-bit JDK 25 build, `MaxMetaspaceSize` defaults to `SIZE_MAX`. In a18container, metadata growth can therefore compete with the whole cgroup before a configured19fail-fast cap is reached; depending on allocation and kernel policy, either a JVM Metaspace20OOM or an external OOM kill may occur. Do not infer one outcome from the missing flag.2122Inspect the target toolchain, JDK build, collector, compressed-pointer mode and deployment23before applying the JDK 25 observations below. JDK 16 introduced Elastic Metaspace, but flags,24allocation granularity and diagnostic layouts vary. This skill does not authorize an upgrade.2526## Workflow27281. **Read the exception text before touching a flag.** `OutOfMemoryError: Metaspace` and29 `OutOfMemoryError: Compressed class space` identify different failed allocation domains.30 Inspect both effective constraints and startup ergonomics; increasing an overall cap is31 not a general repair for an independently exhausted class-space reservation.322. **Compare all memory domains.** Normal heap occupancy does not identify metaspace as33 the cause of RSS growth/OOMKilled; abnormal heap and metadata retention can coexist,34 including Java objects retaining loaders. Correlate cgroup, residency and metadata evidence.353. **Take a time series, not a sample.** Run low-impact `VM.metaspace basic` deliberately,36 use periodic class-loading statistics, and interpret `jdk.MetaspaceSummary` at the GC37 boundaries where it is emitted. Growth in used/committed/classes/loaders plus unload/38 arena-death behavior is the signal; committed alone can reflect policy/fragmentation.394. **Split non-class from class space.** `VM.metaspace` reports `Non-Class`, `Class` and40 `Both` separately. Interpret used, committed, free chunks and waste together; a rising41 waste percentage indicates allocation/chunk inefficiency, not a deterministic prediction42 of which boundary fails first.435. **Decide lifecycle versus capacity.** Normalize load, distinct generated inputs,44 redeploys and warm-up. Plateau does not prove correct sizing; continued growth may be45 legitimate cardinality or delayed unloading. Loader/CLD reachability and generator cache46 keys decide whether it is defective (`jvm-class-loading`).476. **Size from distributions and failure policy.** Cover startup/peak/redeploy/generation48 regimes, class/non-class growth, fragmentation and correlated native peaks. Choose a cap49 that fails before the cgroup only when that fail-fast behavior is desirable; no universal50 `committed × 1.5` margin exists.517. **Classify runtime generation before changing it.** Unbounded retained generation needs52 lifecycle/cardinality control; a legitimate bounded class population may instead need53 capacity. Record raising a ceiling against unresolved growth as mitigation.5455## Rules5657- On the verified 64-bit JDK 25 build, `MaxMetaspaceSize` prints `SIZE_MAX`. Set it only as58 a derived fail-fast/capacity boundary: too low creates avoidable OOM, and it cannot59 guarantee beating an external cgroup kill caused by another domain or transient overlap.60- The compressed class-space reservation is controlled by61 `CompressedClassSpaceSize`; 1073741824 bytes is the verified JDK 25 default, not a62 universal fixed ceiling. Klass metadata lives there; method metadata, constant pools and annotations63 live in the non-class space.64- `UseCompressedClassPointers` is independent of `UseCompressedOops`. Above roughly65 32 GB of heap `UseCompressedOops` turns itself off ergonomically while66 `UseCompressedClassPointers` can stay enabled. Actual class-space reservation depends on67 effective flags, alignment and MaxMetaspaceSize ergonomics; 1 GB is not universal. Check68 flag availability on the exact release rather than using deprecation history as a runtime test.69- `-XX:MetaspaceExpansionSize` does not exist. The real flags are70 `-XX:MinMetaspaceExpansion` (327680 bytes) and `-XX:MaxMetaspaceExpansion`71 (5439488 bytes). `-XX:MetaspaceSize` (22020096 bytes) is the threshold that triggers72 the first metaspace-driven collection, not a size limit.73- Class metadata becomes reclaimable as its CLD unloads; freed chunks can be reused and74 eligible granules can be uncommitted according to Elastic Metaspace policy. Since JEP 38775 (JDK 16+) that return is finer-grained; before it,76 large blocks stayed committed. Do not quote pre-16 behaviour for a JDK 17, 21 or 2577 baseline.78- `System.gc()` does not release a ClassLoader that is still strongly reachable. Remove79 retainers, then verify unloading with the selected collector, flags and collection opportunities;80 reachability changes do not promise immediate collection.81- Distinguish `reserved`, NMT/metaspace `committed`, used, process-resident and cgroup-82 charged in every reading. Committed is not identical to RSS or `memory.current`; reconcile83 timestamps instead of treating it as the bytes the OOM killer sees.84- `jstat -gcmetacapacity` reports `MC` and `CCSC` (the column is `CCSC`, not `CCS`) as85 **capacity**, not usage, and its counters update on internal GC accounting events — a86 freshly started process can report `MC = 0.0` while `VM.metaspace` already shows87 committed memory on some builds. Cross-check `VM.metaspace`; basic output reads live88 counters but is not a guaranteed atomic snapshot of concurrent activity. MC includes class89 and non-class committed space on JDK 25; CCSC is its class-space subset.90- Every non-strong hidden class is its own `ClassLoaderData` with its own chunks —91 3 KB committed for the smallest one on 25.0.3 (`VM.metaspace show-loaders`). Growth from92 runtime generation is classified by the generator's cache key and loader lifetime.93 Lambdas/proxies are commonly code-keyed and plateau; scripts, expressions and per-instance94 proxies can be data-keyed and grow with distinct inputs. Verify the implementation cache.95- CDS and AppCDS can reduce newly allocated metadata when eligible classes are shared; mapped archives appear96 under `Shared class space` in `VM.native_memory`, not as newly committed metaspace.97- None of this applies to a GraalVM `native-image` binary, where classes are frozen at98 build time under its own constraints. HotSpot with a Graal JIT still uses HotSpot metaspace;99 verify build-specific flags and account separately for compiler allocations.100101## References102103- [Reading metaspace from a live JVM](references/reading-metaspace.md) — the `jcmd`104 commands, the real nested `VM.native_memory` layout, and the JFR events confirmed105 against `jfr metadata` on JDK 25. Read before capturing evidence from a running106 process, or when a tool's output does not look like what you expected.107- [Flags, defaults and the sizing protocol](references/sizing-and-flags.md) — measured108 OpenJDK 25 defaults for every metaspace flag and the step-by-step sizing and109 validation procedure. Read when choosing a value for `MaxMetaspaceSize` or110 `CompressedClassSpaceSize`, or when validating that a change worked.111- [Runtime class generation](references/runtime-class-generation.md) — what a generated112 class costs, which generators are bounded by code and which grow with data (lambdas,113 proxies, method handles, mocks, scripting and expression engines), the naming patterns114 that attribute them in `show-loaders`, and the fix per finding. Read when metaspace grows115 in a process that never redeploys, or when `classloader_stats` shows many one-class116 loaders or `+ hidden classes` rows.