RFR: 8367332: Replace BlockTree tree logic with an intrusive red-black tree

Casper Norrbin cnorrbin at openjdk.org
Fri Dec 12 15:49:21 UTC 2025


On Thu, 11 Sep 2025 09:40:21 GMT, Casper Norrbin <cnorrbin at openjdk.org> wrote:

> Hi everyone,
> 
> Metaspace's `BlockTree` is currently implemented as a simple binary search tree, using the memory blocks it stores as the nodes. While this is straightforward and generally fine, it can become unbalanced depending on insert/remove order, leading to degraded performance.
> 
> This is a good fit for the utilities `IntrusiveRBTree`, instead of using internal tree logic. With it, we can replace all tree functions and instead rely on a few insert/remove calls. `BlockTree` still remains as the layer coordinating these blocks, and still handles the list of same-size nodes. The intrusive red-black tree only handles balancing and linking/unlinking nodes in the tree structure. 
> 
> Validation and printing are preserved by using the hooks provided by the red-black tree. We keep the same checks (and get a few more from the rb-tree), and the printed output is kept identical. The only real difference is that the red-black tree traverses the tree instead.
> 
> Testing:
> - Oracle tiers 1-8

I ran a few DaCapo benchmarks as a sanity check to ensure that there were no unexpected side effects from these changes. The VM.info output (expandable section below) shows that the values remain the same, as expected.

<details>
<summary>VM.info output (expandable)</summary>

Output before this patch:

Metaspace:
Metaspace        used 99301K, committed 100608K, reserved 1179648K
 class space     used 11954K, committed 12544K, reserved 1048576K

Usage:
  Non-class:     85.30 MB used.
      Class:     11.67 MB used.
       Both:     96.97 MB used.

Virtual space:
  Non-class space:      128.00 MB reserved,      86.00 MB ( 67%) committed,  2 nodes.
      Class space:        1.00 GB reserved,      12.25 MB (  1%) committed,  1 nodes.
             Both:        1.12 GB reserved,      98.25 MB (  9%) committed. 

Chunk freelists:
   Non-Class:  10.08 MB
       Class:  3.83 MB
        Both:  13.91 MB

num_allocs: 635176.
num_deallocs: 6834.
num_allocs_from_deallocated_blocks: 21675.
num_chunks_retired: 3605.
num_allocs_failed_limit: 9.
num_arena_births: 1780.
num_arena_deaths: 438.
num_vsnodes_births: 3.
num_vsnodes_deaths: 0.
num_space_committed: 1572.
num_space_uncommitted: 0.
num_chunks_returned_to_freelist: 575.
num_chunks_taken_from_freelist: 5386.
num_chunk_merges: 209.
num_chunk_splits: 3640.
num_chunks_enlarged: 5236.
num_inconsistent_stats: 0.


Output after this patch:

Metaspace:
Metaspace        used 99311K, committed 100608K, reserved 1179648K
 class space     used 11951K, committed 12544K, reserved 1048576K

Usage:
  Non-class:     85.31 MB used.
      Class:     11.67 MB used.
       Both:     96.98 MB used.

Virtual space:
  Non-class space:      128.00 MB reserved,      86.00 MB ( 67%) committed,  2 nodes.
      Class space:        1.00 GB reserved,      12.25 MB (  1%) committed,  1 nodes.
             Both:        1.12 GB reserved,      98.25 MB (  9%) committed. 

Chunk freelists:
   Non-Class:  10.31 MB
       Class:  3.83 MB
        Both:  14.14 MB

Internal statistics:

num_allocs: 635534.
num_deallocs: 6709.
num_allocs_from_deallocated_blocks: 21265.
num_chunks_retired: 3600.
num_allocs_failed_limit: 9.
num_arena_births: 1790.
num_arena_deaths: 438.
num_vsnodes_births: 3.
num_vsnodes_deaths: 0.
num_space_committed: 1572.
num_space_uncommitted: 0.
num_chunks_returned_to_freelist: 556.
num_chunks_taken_from_freelist: 5391.
num_chunk_merges: 213.
num_chunk_splits: 3672.
num_chunks_enlarged: 5268.
num_inconsistent_stats: 0.

</details>

To quickly recap how the intrusive tree works: we embed an `IntrusiveNode` inside an external structure. This `IntrusiveNode` is a lightweight data class containing three pointers, used to connect elements in a tree structure. The user must provide comparison functions that operate on the external data, since the intrusive tree neither manages nor allocates memory itself. When a node is added or removed, only the pointers within `IntrusiveNode` are updated.

By swapping the `BlockTree` parent/child pointers to use an `IntrusiveNode`, we are effectively maintaining the same tree structure, just managed differently. The process for inserts/removals remain similar, but we now also balance the tree, which only involves shuffling some pointers. Importantly, no additional memory allocation is done: all relevant data still resides in the outside class (`BlockTree::Node` in this case). The main difference is that pointer management is handled by a separate class, rather than maintaining internal tree logic within `BlockTree`.

-------------

PR Comment: https://git.openjdk.org/jdk/pull/27212#issuecomment-3647082153


More information about the hotspot-runtime-dev mailing list