We delve into the architecture of meta-space. We describe the layers and components and how they work together.

This is interesting for anyone who wants to crack Hotspot and Metaspace or at least really understand where memory goes and why we can’t just use Malloc.

As with most other non-trivial allocators, meta-spaces are implemented in layers.

At the bottom, memory is allocated across the large areas of the operating system. In the middle, we divide these areas into smaller chunks and hand them to the class loader.

At the top, the class loader splits these blocks into calling program code.

The bottom layer of a meta-space: VirtualSpaceList

VirtualSpaceList:

Hg.openjdk.java.net/jdk/jdk11/f…

At the lowest level (at the coarsest granularity), Metaspace’s memory is reserved and committed on demand from the operating system via virtual memory calls like Mmap (3). This happens in a 2MB area (on 64-bit platforms).

These mapping areas are stored as nodes in a global link list named VirtualSpaceList.

Each node manages a high water mark separating committed Spaces from those that remain uncommitted. When the allocation reaches its maximum water mark, new pages are submitted on demand. To avoid calling the operating system too often, a little space is left.

Until the nodes are completely used up. Then, assign a new node and add it to the list. The old node is “failing”.

Memory is allocated from a block node called MetaChunk. They come in three sizes, named SPECIALIZED, Small, and Medium — historically named — and typically 1K/4K/64K

VirtualSpaceList and its nodes are global structures, while Metachunk is owned by a class loader. Therefore, a single node in VirtualSpaceList may contain blocks from different class loaders:

When a class loader and all its associated classes are unloaded, the meta-space used to hold its class metadata is freed. All currently available blocks are added to the global available list (ChunkManager) :

These blocks are reused: If another class loader starts loading classes and allocating meta-space, it might be given a free block instead of allocating a new one:

Metaspace middle layer: Metachunk

The class loader requests memory from Metaspace to get a piece of metadata (usually a small amount, perhaps tens or hundreds of bytes), such as 200 bytes. It will get a Metachunk — achunk of memory that is usually much larger than the requested memory.

Why is that? Because allocating memory directly from global VirtualSpaceList is expensive. VirtualSpaceList is a global structure that needs to be locked. We don’t want to do this too often, so we give the loader a bigger chunk of memory — this Metachunk — that the loader will use to satisfy future allocations faster without locking other loaders. Only when the block runs out does the loader bother global VirtualSpaceList again.

How does the metaspace allocator determine the size of the block to hand to the loader? Well, it’s all speculation:

  • A newly started standard loader will get small 4K blocks until an arbitrary threshold (4) is reached, at which point the meta-space allocator apparently loses patience and starts feeding the loader larger 64K blocks.
  • A bootstrap class loader is called a loader, and it tends to load many classes. So the allocator gives it a huge block (4M) from the start. This can be achieved by InitialBootClassLoaderMetaspaceSize to adjust.
  • Reflection class loader (JDK. Internal. Reflect. DelegatingClassLoader) and anonymous class class loader 3 known only to load a class. Thus, they get a very small (1K) chunk to begin with, since it would be a waste to give them anything else, assuming they won’t need the meta-space any more soon.

Note that this whole optimization — giving the loader more space than it currently needs on the assumption that it will need it soon — is a bet on the loader’s future allocation behavior that may or may not be correct. Once the allocator gives them a chunk, they may stop loading.

This is basically like feeding cats, or small children. The small ones you give a small amount of food on the plate, for the large ones you pile it on, and both cats and children may surprise you at any moment by dropping the spoon (the children, not the cats) and walking away, leaving half-eaten plates of memory behind. The penalty for guessing wrong is wasted memory.

Metaspace upper layer: Metablock Metablock

In Metachunk, we have a second class loader local allocator. It divides the blocks into smaller allocation units. These units are called meta-blocks and are the actual units passed to the caller (for example, the meta-block contains an InstanceKlass).

Such loaders local allocators can be raw and therefore fast:

The lifetime of class metadata is bound to the class loader, and when the class loader dies, it is released in bulk. Therefore, the JVM does not need to care about releasing random tuple 4. Unlike the general purpose Malloc (3) allocator.

Let’s examine Metachunk:

When it is born, it contains only the head. Subsequent allocations are only at the top. Since the whole block of metadata can be freed, it can no longer rely on the allocation of the whole block.

Note the “unused” part of the current block: since the block belongs to a class loader, this part can only be used by the same loader. If the loader stops loading the class, this space is actually wasted.

ClassloaderData and ClassLoaderMetaspace

The class loader stores its native representation in a native structure called ClassLoaderData.

This structure references a ClassLoaderMetaspace structure that holds a list of all meta blocks used by the loader.

When the loader is unloaded, the associated ClassLoaderData and its ClassLoaderMetaspace are deleted. This frees all blocks used by the class loader into the meta-space free list. If the condition is correct, this may or may not result in memory being freed to the operating system, see :javakk.com/160.html

An anonymous class

Classloader data! =ClassLoaderMetaspace

Note that we’ve been saying that “the meta-space memory is owned by its classloader” — but we’re lying a bit here, and this is a simplification. The situation gets more complicated with the addition of anonymous classes:

These are the constructs generated for dynamic language support. When the loader loads an anonymous class, the class gets its own separate ClassLoaderData, whose lifetime is coupled to the lifetime of the anonymous class, not the host class loader (thus, it and its associated metadata can be collected before the Housing Loader is collected). This means that the class loader has a primary class loader data for all normally loaded classes, and a secondary class loader data structure for each anonymous class.

The purpose of this separation is to unnecessarily extend the life of meta-space allocations such as Lambdas and method handles.

So, again: When does memory return to the operating system?

Let’s see again when memory is returned to the operating system. We can now answer this question in more detail than we did at the end of Part 1:

When all blocks in a VirtualSpaceListNode happen to be free, the node itself is removed. This object will be removed from VirtualSpaceList. Its free block is removed from the Metaspace free list. The node is unmapped and its memory is returned to the operating system. The node is cleared.

In order for all blocks in a node to be free, all class loaders that own those blocks must be dead.

Whether this is possible depends largely on fragmentation:

The size of a node is 2MB; Block sizes range from 1K to 64K; The typical load per node is 150-200 blocks. If these blocks are all allocated by a class loader, collecting the loader frees the node and its memory to the operating system.

However, if the blocks are owned by different class loaders with different life cycles, nothing is released. This may occur when we are dealing with many small class loaders, such as loaders for anonymous classes or reflection delegates.

Also, note that part of Metaspace (compressed class space) will never be released back into the operating system.

  • Memory is reserved by the operating system in a 2MB area and stored in the global link list. These areas are committed to providing services on demand.
  • These areas are divided into blocks and handed to the class loader. The block belongs to a class loader.
  • Blocks are further divided into tiny allocations called blocks. These are the distribution units that are distributed to callers.
  • When a global block is reused, it owns a global block. Some of the memory may be freed up into the operating system, but much depends on fragmentation and luck.

Source: javakk.com/395.html