Note: this Wiki page describes Metaspace in its current form, which has substantially changed in the wake of JEP 387 Elastic Metaspace. Some information in this page may not be applicable for earlier JDK releases.
What is Metaspace?
Metaspace is a native (as in, off-java-heap) memory manager in the hotspot. It is used to manage memory for class metadata.
Class metadata are allocated when classes are loaded. Their lifetime is usually scoped to that of the loading classloader. When a loader gets collected, all class metadata it accumulated are released in bulk.
Therefore it follows a bulk-delete scenario, and so the memory manager does not need to track individual allocations for the purpose of freeing them. Hence, metaspace is an Arena- or Region-Based Allocator. It is optimized for fast, very memory efficient allocation of native memory at the cost of not being able to (easily) delete arbitrary blocks.
High-level functional overview
A CLD (ClassLoaderData
) instance owns a MetaspaceArena
. From that arena it allocates memory for class metadata and other purposes via pointer bump. As it is used up, the arena grows dynamically in semi-coarse steps. When the class loader is unloaded, its CLD is deleted, the arena gets deleted and its memory returned to the metaspace. This memory is kept inside metaspace for later re-use, but metaspace may decide to uncommit parts or all of it as it sees fit.
Globally there exist a `MetaspaceContext`: it manages the underlying memory at the OS level. To arenas it offers a coarse-grained allocation API, which hands out memory in the form of chunks. It also keeps a freelist of said chunks which had been released from deceased arenas.
Only one global context exists if compressed class pointers are disabled and we have no compressed class space. If compressed class pointers are enabled, we keep class space allocations separate from non-class space allocations. So we have two global metaspace contexts: one holding allocations of Klass structures (the "compressed class space"), one holding everything else (the "non-class" metaspace). Mirroring that duality, each CLD now owns two arenas as well.
![High Level Overview, compressed class space disabled](./highlevel-overview-no-ccs.svg "High Level Overview, compressed class space disabled")
![High Level Overview, compressed class space enabled](./highlevel-overview-ccs.svg "High Level Overview, compressed class space enabled")
Core Concepts
Commit Granules
One of the key points of Elastic Metaspace is elasticity, the ability to return unneeded memory to the OS, and commit memory only on demand.
Metaspace address space is divided into homogeneously power-of-two sized memory units called commit granules. Commit granules are the basic unit of committing and uncommitting memory in Metaspace and therefore dictate the coarseness of committing.
While commit granules may be technically as small as a single page, in practice they are larger (defaulting to 64K). When memory is returned to the metaspace, commit granules which are completely unoccupied are uncommitted.
The commit granule size is a trade-off between efficiency of memory reclamation and certain costs associated with fragmenting the memory map. The smaller a granule is, the more likely it is to be unoccupied and eligible for uncommitting, but at the same time, uncommitting many small areas will increase the number of mappings of the VM process. The default size is 64K, which is a compromise which seems to work very well, only moderately increase the number of mappings while giving us good elasticity.
Granule size can indirectly be influenced via the MetaspaceReclaimStrategy
switch (see below).
Metachunks and the Buddy Style Allocator
Metaspace arenas will dynamically grow, in semi-coarse steps. Internally they are lists of variable-sized memory areas called Metachunk (see metachunk.hpp
). Arenas obtain these chunks from their respective metaspace context, to which they return all chunks in bulk when they die.
Chunks are variable power-of-two sized, ranging from the largest possible chunk size, 4M - the Root Chunk - down to the smallest chunk size of 1K.
Chunks are managed by a power-two-based buddy allocator. A buddy allocator is very efficient in keeping fragmentation at bay, at the cost of limiting the size of managed areas to power of two units. This restriction does not matter in metaspace since these chunks are not the ultimate - user level - unit of allocation, just an intermediate.
Throughout the metaspace implementation, chunk size is indicated not as size but given as "chunk level" (chunklevel_t
, see chunklevel.hpp
). A root chunk has chunk level 0, the next smaller chunk level 1 and so on, down to the smallest chunk with level 13. Helper functions and constants to work with chunk level can be found at chunk_level.hpp.
+------ ~~~~ --------+
| | level 0: (4M root chunk)
+------ ~~~~ --------+
...
+--------------+
| | level 9: 8K
+--------------+
+------+
| | level 10: 4K
+------+
+--+
| | level 11: 2K
+--+
++
|| level 12: 1K (smallest chunk)
++
In buddy style allocation, a chunk is always one part of a neighboring pair of chunks, unless the chunk is a root chunk. In code we use the term leader for the chunk with the lower address of the pair, his partner is called follower.
+-------------------+-------------------+
| Leader | Follower |
+-------------------+-------------------+
One or both of which could be split into smaller chunks, of course.
+-------------------+-------------------+
| Leader | | | |
+-------------------+-------------------+
Merging chunks
A free chunk can be merged with its buddy if that buddy is free and unsplit. This is done recursively, until either one of the partners in the chunk pair are not free and unsplit, or until the largest chunk size - root chunk size - is reached.
This crystallizes a range of free chunks into one larger chunk quite effectively. In the following figure, chunk b becomes free, melds with free chunk A, then with chunk C:
+---------+---------+-------------------+
| A | b | C |
+---------+---------+-------------------+
|
v
+-------------------+-------------------+
| A` | C |
+-------------------+-------------------+
|
v
+-------------------+-------------------+
| A`` |
+-------------------+-------------------+
Splitting chunks
To get a small chunk from a larger chunk, a large chunk can be split. Splitting happens in power-of-2 sizes. A split operation yields the desired smaller chunk as well as 1-n splinter chunks.
Step
+---------------------------------------+
0 | A |
+---------------------------------------+
|
v
+-------------------+-------------------+
1 | d1 | D2 | C | B |
+-------------------+-------------------+
^
Result chunk
|
v
+-------------------+-------------------+
2 | d1 | d2 | c | B |
+-------------------+-------------------+
^
Result chunk
|
v
+-------------------+-------------------+
3 | d1 | d2 | c1 | C2 | B |
+-------------------+-------------------+
^
Result chunk
How it all looks in memory
Allocated metaspace blocks (the user-level unit of allocation) reside in chunks; chunks reside in mappings called VirtualSpaceNode
, of which multiple may exist:
+------------------+ <--- virtual memory region
| +-------------+ | <--- chunk
| | +---------+ | | <--- block
| | | | | |
| | +---------+ | |
| | +---------+ | | <--- block
| | | | | |
| | | | | |
| | | | | |
| | +---------+ | |
| | +---------+ | | <--- block
| | +---------+ | |
| | | |
| +-------------+ | <--- end: chunk
| +-------------+ | <--- chunk
| | +---------+ | |
| | | | | |
...
+------------------+ <--- end: virtual memory region
+------------------+ <--- next virtual memory region
| +-------------+ |
| | +---------+ | |
| | | | | |
| | +---------+ | |
...