diff a/test/hotspot/gtest/metaspace/test_clms.cpp b/test/hotspot/gtest/metaspace/test_clms.cpp
--- /dev/null
+++ b/test/hotspot/gtest/metaspace/test_clms.cpp
@@ -0,0 +1,419 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023 Red Hat, Inc. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ *
+ */
+
+#include "precompiled.hpp"
+#include "memory/classLoaderMetaspace.hpp"
+#include "memory/metaspace/freeBlocks.hpp"
+#include "memory/metaspace/metablock.inline.hpp"
+#include "memory/metaspace/metaspaceArena.hpp"
+#include "memory/metaspace/metaspaceSettings.hpp"
+#include "memory/metaspace/metaspaceStatistics.hpp"
+#include "memory/metaspace.hpp"
+#include "oops/klass.hpp"
+#include "runtime/mutex.hpp"
+#include "utilities/debug.hpp"
+#include "utilities/align.hpp"
+#include "utilities/globalDefinitions.hpp"
+
+#ifdef _LP64
+
+#define LOG_PLEASE
+#include "metaspaceGtestCommon.hpp"
+#include "metaspaceGtestContexts.hpp"
+#include "metaspaceGtestRangeHelpers.hpp"
+#include "metaspaceGtestSparseArray.hpp"
+
+#define HANDLE_FAILURE \
+  if (testing::Test::HasFailure()) { \
+    return; \
+  }
+
+namespace metaspace {
+
+class ClmsTester {
+
+  Mutex _lock;
+  MetaspaceContext* _class_context;
+  MetaspaceContext* _nonclass_context;
+  ClassLoaderMetaspace* _clms;
+  const size_t _klass_arena_alignment_words;
+  unsigned _num_allocations;
+
+  struct Deltas {
+    int num_chunks_delta;
+    ssize_t used_words_delta;
+    int num_freeblocks_delta;
+    ssize_t freeblocks_words_delta;
+  };
+
+  Deltas calc_deltas(const ArenaStats& before, const ArenaStats& after) {
+    Deltas d;
+    d.num_chunks_delta = after.totals()._num - before.totals()._num;
+    d.used_words_delta = after.totals()._used_words - before.totals()._used_words;
+    d.num_freeblocks_delta = (int)after._free_blocks_num - (int)before._free_blocks_num;
+    d.freeblocks_words_delta = after._free_blocks_word_size - before._free_blocks_word_size;
+    return d;
+  }
+
+public:
+
+  ClmsTester(size_t klass_alignment_words, Metaspace::MetaspaceType space_type,
+             MetaspaceContext* class_context, MetaspaceContext* nonclass_context)
+  : _lock(Monitor::nosafepoint, "CLMSTest_lock"),
+    _class_context(class_context), _nonclass_context(nonclass_context),
+    _clms(nullptr), _klass_arena_alignment_words(klass_alignment_words), _num_allocations(0) {
+    _clms = new ClassLoaderMetaspace(&_lock, space_type, nonclass_context, class_context, klass_alignment_words);
+  }
+
+  ~ClmsTester() {
+    delete _clms;
+    EXPECT_EQ(_class_context->used_words(), (size_t)0);
+    EXPECT_EQ(_nonclass_context->used_words(), (size_t)0);
+  }
+
+  MetaBlock allocate_and_check(size_t word_size, bool is_class) {
+
+    // take stats before allocation
+    ClmsStats stats_before;
+    _clms->add_to_statistics(&stats_before);
+
+    // allocate
+    MetaWord* p = _clms->allocate(word_size, is_class ? Metaspace::ClassType : Metaspace::NonClassType);
+    _num_allocations ++;
+
+    // take stats after allocation
+    ClmsStats stats_after;
+    _clms->add_to_statistics(&stats_after);
+
+    // for less verbose testing:
+    const ArenaStats& ca_before = stats_before._arena_stats_class;
+    const ArenaStats& ca_after = stats_after._arena_stats_class;
+    const ArenaStats& nca_before = stats_before._arena_stats_nonclass;
+    const ArenaStats& nca_after = stats_after._arena_stats_nonclass;
+
+    // deltas
+    const Deltas d_ca = calc_deltas(ca_before, ca_after);
+    const Deltas d_nca = calc_deltas(nca_before, nca_after);
+
+#define EXPECT_FREEBLOCKS_UNCHANGED(arena_prefix) \
+    EXPECT_EQ(d_##arena_prefix.num_freeblocks_delta, 0);  \
+    EXPECT_EQ(d_##arena_prefix.freeblocks_words_delta, (ssize_t)0);
+
+#define EXPECT_ARENA_UNCHANGED(arena_prefix) \
+    EXPECT_EQ(d_##arena_prefix.num_chunks_delta, 0);  \
+    EXPECT_EQ(d_##arena_prefix.used_words_delta, (ssize_t)0);
+
+    if (p != nullptr) {
+
+      MetaBlock bl(p, word_size);
+
+      if (is_class) {
+
+        EXPECT_TRUE(bl.is_aligned_base(_klass_arena_alignment_words));
+
+        if (_num_allocations == 1) {
+          // first allocation: nonclass arena unchanged, class arena grows by 1 chunk and wordsize,
+          // class arena freeblocks unchanged
+          EXPECT_ARENA_UNCHANGED(nca);
+          EXPECT_FREEBLOCKS_UNCHANGED(nca);
+          EXPECT_EQ(d_ca.num_chunks_delta, 1);
+          EXPECT_EQ((size_t)d_ca.used_words_delta, word_size);
+          EXPECT_FREEBLOCKS_UNCHANGED(ca);
+          return bl;
+        }
+
+        // Had this been taken from class arena freeblocks?
+        if (d_ca.num_freeblocks_delta == -1) {
+          // the class arena freeblocks should have gone down, and the non-class arena freeblocks may have gone
+          // up in case the block was larger than required
+          const size_t wordsize_block_taken = (size_t)(-d_ca.freeblocks_words_delta);
+          EXPECT_GE(wordsize_block_taken, word_size); // the block we took must be at least allocation size
+          const size_t expected_freeblock_remainder = wordsize_block_taken - word_size;
+          if (expected_freeblock_remainder > 0) {
+            // the remainder, if it existed, should have been added to nonclass freeblocks
+            EXPECT_EQ(d_nca.num_freeblocks_delta, 1);
+            EXPECT_EQ((size_t)d_nca.freeblocks_words_delta, expected_freeblock_remainder);
+          }
+          // finally, nothing should have happened in the arenas proper.
+          EXPECT_ARENA_UNCHANGED(ca);
+          EXPECT_ARENA_UNCHANGED(nca);
+          return bl;
+        }
+
+        // block was taken from class arena proper
+
+        // We expect allocation waste due to alignment, should have been added to the freeblocks
+        // of nonclass arena. Allocation waste can be 0. If no chunk turnover happened, it must be
+        // smaller than klass alignment, otherwise it can get as large as a commit granule.
+        const size_t max_expected_allocation_waste =
+            d_ca.num_chunks_delta == 0 ? (_klass_arena_alignment_words - 1) : Settings::commit_granule_words();
+        EXPECT_GE(d_ca.num_chunks_delta, 0);
+        EXPECT_LE(d_ca.num_chunks_delta, 1);
+        EXPECT_GE((size_t)d_ca.used_words_delta, word_size);
+        EXPECT_LE((size_t)d_ca.used_words_delta, word_size + max_expected_allocation_waste);
+        EXPECT_FREEBLOCKS_UNCHANGED(ca);
+        EXPECT_ARENA_UNCHANGED(nca);
+        if (max_expected_allocation_waste > 0) {
+          EXPECT_GE(d_nca.num_freeblocks_delta, 0);
+          EXPECT_LE(d_nca.num_freeblocks_delta, 1);
+          EXPECT_GE(d_nca.freeblocks_words_delta, 0);
+          EXPECT_LE((size_t)d_nca.freeblocks_words_delta, max_expected_allocation_waste);
+        } else {
+          EXPECT_FREEBLOCKS_UNCHANGED(nca);
+        }
+        return bl;
+
+      } // end: is_class
+
+      else
+
+      {
+        // Nonclass arena allocation.
+        // Allocation waste can happen:
+        // - if we allocate from nonclass freeblocks, the block remainder
+        // - if we allocate from arena proper, by chunk turnover
+
+        if (d_nca.freeblocks_words_delta < 0) {
+          // We allocated a block from the nonclass arena freeblocks.
+          const size_t wordsize_block_taken = (size_t)(-d_nca.freeblocks_words_delta);
+          EXPECT_EQ(wordsize_block_taken, word_size);
+          // The number of blocks may or may not have decreased (depending on whether there
+          // was a wastage block)
+          EXPECT_GE(d_nca.num_chunks_delta, -1);
+          EXPECT_LE(d_nca.num_chunks_delta, 0);
+          EXPECT_ARENA_UNCHANGED(nca);
+          EXPECT_ARENA_UNCHANGED(ca);
+          EXPECT_FREEBLOCKS_UNCHANGED(ca);
+          return bl;
+        }
+
+        // We don't expect alignment waste. Only wastage happens at chunk turnover.
+        const size_t max_expected_allocation_waste =
+            d_nca.num_chunks_delta == 0 ? 0 : Settings::commit_granule_words();
+        EXPECT_ARENA_UNCHANGED(ca);
+        EXPECT_FREEBLOCKS_UNCHANGED(ca);
+        EXPECT_GE(d_nca.num_chunks_delta, 0);
+        EXPECT_LE(d_nca.num_chunks_delta, 1);
+        EXPECT_GE((size_t)d_nca.used_words_delta, word_size);
+        EXPECT_LE((size_t)d_nca.used_words_delta, word_size + max_expected_allocation_waste);
+        if (max_expected_allocation_waste == 0) {
+          EXPECT_FREEBLOCKS_UNCHANGED(nca);
+        }
+      }
+      return bl;
+
+    } // end: allocation successful
+
+    // allocation failed.
+    EXPECT_ARENA_UNCHANGED(ca);
+    EXPECT_FREEBLOCKS_UNCHANGED(ca);
+    EXPECT_ARENA_UNCHANGED(nca);
+    EXPECT_FREEBLOCKS_UNCHANGED(nca);
+
+    return MetaBlock();
+  }
+
+  MetaBlock allocate_expect_success(size_t word_size, bool is_class) {
+    MetaBlock bl = allocate_and_check(word_size, is_class);
+    EXPECT_TRUE(bl.is_nonempty());
+    return bl;
+  }
+
+  MetaBlock allocate_expect_failure(size_t word_size, bool is_class) {
+    MetaBlock bl = allocate_and_check(word_size, is_class);
+    EXPECT_TRUE(bl.is_empty());
+    return bl;
+  }
+
+  void deallocate_and_check(MetaBlock bl, bool is_class) {
+
+    // take stats before deallocation
+    ClmsStats stats_before;
+    _clms->add_to_statistics(&stats_before);
+
+    // allocate
+    _clms->deallocate(bl.base(), bl.word_size(), is_class);
+
+    // take stats after deallocation
+    ClmsStats stats_after;
+    _clms->add_to_statistics(&stats_after);
+
+    // for less verbose testing:
+    const ArenaStats& ca_before = stats_before._arena_stats_class;
+    const ArenaStats& ca_after = stats_after._arena_stats_class;
+    const ArenaStats& nca_before = stats_before._arena_stats_nonclass;
+    const ArenaStats& nca_after = stats_after._arena_stats_nonclass;
+
+    // deltas
+    // deltas
+    const Deltas d_ca = calc_deltas(ca_before, ca_after);
+    const Deltas d_nca = calc_deltas(nca_before, nca_after);
+
+    EXPECT_ARENA_UNCHANGED(ca);
+    EXPECT_ARENA_UNCHANGED(nca);
+    if (is_class) {
+      EXPECT_EQ(d_ca.num_freeblocks_delta, 1);
+      EXPECT_EQ((size_t)d_ca.freeblocks_words_delta, bl.word_size());
+      EXPECT_FREEBLOCKS_UNCHANGED(nca);
+    } else {
+      EXPECT_EQ(d_nca.num_freeblocks_delta, 1);
+      EXPECT_EQ((size_t)d_nca.freeblocks_words_delta, bl.word_size());
+      EXPECT_FREEBLOCKS_UNCHANGED(ca);
+    }
+  }
+};
+
+static constexpr size_t klass_size = sizeof(Klass) / BytesPerWord;
+
+static void basic_test(size_t klass_arena_alignment) {
+  if (Settings::use_allocation_guard()) {
+    return;
+  }
+  MetaspaceGtestContext class_context, nonclass_context;
+  {
+    ClmsTester tester(klass_arena_alignment, Metaspace::StandardMetaspaceType, class_context.context(), nonclass_context.context());
+
+    MetaBlock bl1 = tester.allocate_expect_success(klass_size, true);
+    HANDLE_FAILURE;
+
+    MetaBlock bl2 = tester.allocate_expect_success(klass_size, true);
+    HANDLE_FAILURE;
+
+    tester.deallocate_and_check(bl1, true);
+    HANDLE_FAILURE;
+
+    MetaBlock bl3 = tester.allocate_expect_success(klass_size, true);
+    HANDLE_FAILURE;
+    EXPECT_EQ(bl3, bl1); // should have gotten the same block back from freelist
+
+    MetaBlock bl4 = tester.allocate_expect_success(Metaspace::min_allocation_word_size, false);
+    HANDLE_FAILURE;
+
+    MetaBlock bl5 = tester.allocate_expect_success(K, false);
+    HANDLE_FAILURE;
+
+    tester.deallocate_and_check(bl5, false);
+    HANDLE_FAILURE;
+
+    MetaBlock bl6 = tester.allocate_expect_success(K, false);
+    HANDLE_FAILURE;
+
+    EXPECT_EQ(bl5, bl6); // should have gotten the same block back from freelist
+  }
+  EXPECT_EQ(class_context.used_words(), (size_t)0);
+  EXPECT_EQ(nonclass_context.used_words(), (size_t)0);
+  // we should have used exactly one commit granule (64K), not more, for each context
+  EXPECT_EQ(class_context.committed_words(), Settings::commit_granule_words());
+  EXPECT_EQ(nonclass_context.committed_words(), Settings::commit_granule_words());
+}
+
+#define TEST_BASIC_N(n)               \
+TEST_VM(metaspace, CLMS_basics_##n) { \
+  basic_test(n);            \
+}
+
+TEST_BASIC_N(1)
+TEST_BASIC_N(4)
+TEST_BASIC_N(16)
+TEST_BASIC_N(32)
+TEST_BASIC_N(128)
+
+static void test_random(size_t klass_arena_alignment) {
+  if (Settings::use_allocation_guard()) {
+    return;
+  }
+
+  MetaspaceGtestContext class_context, nonclass_context;
+  constexpr int max_allocations = 1024;
+  const SizeRange nonclass_alloc_range(Metaspace::min_allocation_alignment_words, 1024);
+  const SizeRange class_alloc_range(klass_size, 1024);
+  const IntRange one_out_of_ten(0, 10);
+  for (int runs = 9; runs >= 0; runs--) {
+    {
+      ClmsTester tester(64, Metaspace::StandardMetaspaceType, class_context.context(), nonclass_context.context());
+      struct LifeBlock {
+        MetaBlock bl;
+        bool is_class;
+      };
+      LifeBlock life_allocations[max_allocations];
+      for (int i = 0; i < max_allocations; i++) {
+        life_allocations[i].bl.reset();
+      }
+
+      unsigned num_class_allocs = 0, num_nonclass_allocs = 0, num_class_deallocs = 0, num_nonclass_deallocs = 0;
+      for (int i = 0; i < 5000; i ++) {
+        const int slot = IntRange(0, max_allocations).random_value();
+        if (life_allocations[slot].bl.is_empty()) {
+          const bool is_class = one_out_of_ten.random_value() == 0;
+          const size_t word_size =
+              is_class ? class_alloc_range.random_value() : nonclass_alloc_range.random_value();
+          MetaBlock bl = tester.allocate_expect_success(word_size, is_class);
+          HANDLE_FAILURE;
+          life_allocations[slot].bl = bl;
+          life_allocations[slot].is_class = is_class;
+          if (is_class) {
+            num_class_allocs ++;
+          } else {
+            num_nonclass_allocs ++;
+          }
+        } else {
+          tester.deallocate_and_check(life_allocations[slot].bl, life_allocations[slot].is_class);
+          HANDLE_FAILURE;
+          life_allocations[slot].bl.reset();
+          if (life_allocations[slot].is_class) {
+            num_class_deallocs ++;
+          } else {
+            num_nonclass_deallocs ++;
+          }
+        }
+      }
+      LOG("num class allocs: %u, num nonclass allocs: %u, num class deallocs: %u, num nonclass deallocs: %u",
+          num_class_allocs, num_nonclass_allocs, num_class_deallocs, num_nonclass_deallocs);
+    }
+    EXPECT_EQ(class_context.used_words(), (size_t)0);
+    EXPECT_EQ(nonclass_context.used_words(), (size_t)0);
+    constexpr float fragmentation_factor = 3.0f;
+    const size_t max_expected_nonclass_committed = max_allocations * nonclass_alloc_range.highest() * fragmentation_factor;
+    const size_t max_expected_class_committed = max_allocations * class_alloc_range.highest() * fragmentation_factor;
+    // we should have used exactly one commit granule (64K), not more, for each context
+    EXPECT_LT(class_context.committed_words(), max_expected_class_committed);
+    EXPECT_LT(nonclass_context.committed_words(), max_expected_nonclass_committed);
+  }
+}
+
+#define TEST_RANDOM_N(n)               \
+TEST_VM(metaspace, CLMS_random_##n) {  \
+  test_random(n);                      \
+}
+
+TEST_RANDOM_N(1)
+TEST_RANDOM_N(4)
+TEST_RANDOM_N(16)
+TEST_RANDOM_N(32)
+TEST_RANDOM_N(128)
+
+} // namespace metaspace
+
+#endif // _LP64
