diff --git a/src/hotspot/share/prims/jvmtiImpl.cpp b/src/hotspot/share/prims/jvmtiImpl.cpp
index ac7143e02b8..8ab7f098542 100644
--- a/src/hotspot/share/prims/jvmtiImpl.cpp
+++ b/src/hotspot/share/prims/jvmtiImpl.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2024, Oracle and/or its affiliates. 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
@@ -41,6 +41,7 @@
 #include "prims/jvmtiEventController.inline.hpp"
 #include "prims/jvmtiImpl.hpp"
 #include "prims/jvmtiRedefineClasses.hpp"
+#include "runtime/atomic.hpp"
 #include "runtime/continuation.hpp"
 #include "runtime/deoptimization.hpp"
 #include "runtime/frame.inline.hpp"
@@ -89,103 +90,6 @@ JvmtiAgentThread::call_start_function() {
     _start_fn(_env->jvmti_external(), jni_environment(), (void*)_start_arg);
 }
 
-
-//
-// class GrowableCache - private methods
-//
-
-void GrowableCache::recache() {
-  int len = _elements->length();
-
-  FREE_C_HEAP_ARRAY(address, _cache);
-  _cache = NEW_C_HEAP_ARRAY(address,len+1, mtInternal);
-
-  for (int i=0; i<len; i++) {
-    _cache[i] = _elements->at(i)->getCacheValue();
-    //
-    // The cache entry has gone bad. Without a valid frame pointer
-    // value, the entry is useless so we simply delete it in product
-    // mode. The call to remove() will rebuild the cache again
-    // without the bad entry.
-    //
-    if (_cache[i] == nullptr) {
-      assert(false, "cannot recache null elements");
-      remove(i);
-      return;
-    }
-  }
-  _cache[len] = nullptr;
-
-  _listener_fun(_this_obj,_cache);
-}
-
-//
-// class GrowableCache - public methods
-//
-
-GrowableCache::GrowableCache() {
-  _this_obj       = nullptr;
-  _listener_fun   = nullptr;
-  _elements       = nullptr;
-  _cache          = nullptr;
-}
-
-GrowableCache::~GrowableCache() {
-  clear();
-  delete _elements;
-  FREE_C_HEAP_ARRAY(address, _cache);
-}
-
-void GrowableCache::initialize(void *this_obj, void listener_fun(void *, address*) ) {
-  _this_obj       = this_obj;
-  _listener_fun   = listener_fun;
-  _elements       = new (mtServiceability) GrowableArray<GrowableElement*>(5, mtServiceability);
-  recache();
-}
-
-// number of elements in the collection
-int GrowableCache::length() {
-  return _elements->length();
-}
-
-// get the value of the index element in the collection
-GrowableElement* GrowableCache::at(int index) {
-  GrowableElement *e = (GrowableElement *) _elements->at(index);
-  assert(e != nullptr, "e != nullptr");
-  return e;
-}
-
-int GrowableCache::find(const GrowableElement* e) const {
-  return _elements->find_if([&](const GrowableElement* other_e) { return e->equals(other_e); });
-}
-
-// append a copy of the element to the end of the collection
-void GrowableCache::append(GrowableElement* e) {
-  GrowableElement *new_e = e->clone();
-  _elements->append(new_e);
-  recache();
-}
-
-// remove the element at index
-void GrowableCache::remove (int index) {
-  GrowableElement *e = _elements->at(index);
-  assert(e != nullptr, "e != nullptr");
-  _elements->remove(e);
-  delete e;
-  recache();
-}
-
-// clear out all elements, release all heap space and
-// let our listener know that things have changed.
-void GrowableCache::clear() {
-  int len = _elements->length();
-  for (int i=0; i<len; i++) {
-    delete _elements->at(i);
-  }
-  _elements->clear();
-  recache();
-}
-
 //
 // class JvmtiBreakpoint
 //
@@ -194,20 +98,19 @@ JvmtiBreakpoint::JvmtiBreakpoint(Method* m_method, jlocation location)
     : _method(m_method), _bci((int)location) {
   assert(_method != nullptr, "No method for breakpoint.");
   assert(_bci >= 0, "Negative bci for breakpoint.");
-  oop class_holder_oop  = _method->method_holder()->klass_holder();
+  oop class_holder_oop = _method->method_holder()->klass_holder();
   _class_holder = OopHandle(JvmtiExport::jvmti_oop_storage(), class_holder_oop);
 }
 
+JvmtiBreakpoint::JvmtiBreakpoint(const JvmtiBreakpoint& bp)
+    : _method(bp._method), _bci(bp._bci) {
+  _class_holder = OopHandle(JvmtiExport::jvmti_oop_storage(), bp._class_holder.resolve());
+}
+
 JvmtiBreakpoint::~JvmtiBreakpoint() {
   _class_holder.release(JvmtiExport::jvmti_oop_storage());
 }
 
-void JvmtiBreakpoint::copy(JvmtiBreakpoint& bp) {
-  _method   = bp._method;
-  _bci      = bp._bci;
-  _class_holder = OopHandle(JvmtiExport::jvmti_oop_storage(), bp._class_holder.resolve());
-}
-
 bool JvmtiBreakpoint::equals(const JvmtiBreakpoint& bp) const {
   return _method   == bp._method
     &&   _bci      == bp._bci;
@@ -301,8 +204,8 @@ void VM_ChangeBreakpoints::doit() {
 // a JVMTI internal collection of JvmtiBreakpoint
 //
 
-JvmtiBreakpoints::JvmtiBreakpoints(void listener_fun(void *,address *)) {
-  _bps.initialize(this,listener_fun);
+JvmtiBreakpoints::JvmtiBreakpoints()
+    : _elements(5, mtServiceability) {
 }
 
 JvmtiBreakpoints:: ~JvmtiBreakpoints() {}
@@ -312,9 +215,9 @@ void JvmtiBreakpoints::print() {
   LogTarget(Trace, jvmti) log;
   LogStream log_stream(log);
 
-  int n = _bps.length();
-  for (int i=0; i<n; i++) {
-    JvmtiBreakpoint& bp = _bps.at(i);
+  int n = length();
+  for (int i = 0; i < n; i++) {
+    JvmtiBreakpoint& bp = at(i);
     log_stream.print("%d: ", i);
     bp.print_on(&log_stream);
     log_stream.cr();
@@ -326,9 +229,9 @@ void JvmtiBreakpoints::print() {
 void JvmtiBreakpoints::set_at_safepoint(JvmtiBreakpoint& bp) {
   assert(SafepointSynchronize::is_at_safepoint(), "must be at safepoint");
 
-  int i = _bps.find(bp);
+  int i = find(bp);
   if (i == -1) {
-    _bps.append(bp);
+    append(bp);
     bp.set();
   }
 }
@@ -336,18 +239,16 @@ void JvmtiBreakpoints::set_at_safepoint(JvmtiBreakpoint& bp) {
 void JvmtiBreakpoints::clear_at_safepoint(JvmtiBreakpoint& bp) {
   assert(SafepointSynchronize::is_at_safepoint(), "must be at safepoint");
 
-  int i = _bps.find(bp);
+  int i = find(bp);
   if (i != -1) {
-    _bps.remove(i);
+    remove(i);
     bp.clear();
   }
 }
 
-int JvmtiBreakpoints::length() { return _bps.length(); }
-
 int JvmtiBreakpoints::set(JvmtiBreakpoint& bp) {
-  if ( _bps.find(bp) != -1) {
-     return JVMTI_ERROR_DUPLICATE;
+  if (find(bp) != -1) {
+    return JVMTI_ERROR_DUPLICATE;
   }
   VM_ChangeBreakpoints set_breakpoint(VM_ChangeBreakpoints::SET_BREAKPOINT, &bp);
   VMThread::execute(&set_breakpoint);
@@ -355,8 +256,8 @@ int JvmtiBreakpoints::set(JvmtiBreakpoint& bp) {
 }
 
 int JvmtiBreakpoints::clear(JvmtiBreakpoint& bp) {
-  if ( _bps.find(bp) == -1) {
-     return JVMTI_ERROR_NOT_FOUND;
+  if (find(bp) == -1) {
+    return JVMTI_ERROR_NOT_FOUND;
   }
 
   VM_ChangeBreakpoints clear_breakpoint(VM_ChangeBreakpoints::CLEAR_BREAKPOINT, &bp);
@@ -365,27 +266,14 @@ int JvmtiBreakpoints::clear(JvmtiBreakpoint& bp) {
 }
 
 void JvmtiBreakpoints::clearall_in_class_at_safepoint(Klass* klass) {
-  bool changed = true;
-  // We are going to run thru the list of bkpts
-  // and delete some.  This deletion probably alters
-  // the list in some implementation defined way such
-  // that when we delete entry i, the next entry might
-  // no longer be at i+1.  To be safe, each time we delete
-  // an entry, we'll just start again from the beginning.
-  // We'll stop when we make a pass thru the whole list without
-  // deleting anything.
-  while (changed) {
-    int len = _bps.length();
-    changed = false;
-    for (int i = 0; i < len; i++) {
-      JvmtiBreakpoint& bp = _bps.at(i);
-      if (bp.method()->method_holder() == klass) {
-        bp.clear();
-        _bps.remove(i);
-        // This changed 'i' so we have to start over.
-        changed = true;
-        break;
-      }
+  assert(SafepointSynchronize::is_at_safepoint(), "must be at safepoint");
+
+  // Go backwards because this removes entries that are freed.
+  for (int i = length() - 1; i >= 0; i--) {
+    JvmtiBreakpoint& bp = at(i);
+    if (bp.method()->method_holder() == klass) {
+      bp.clear();
+      remove(i);
     }
   }
 }
@@ -395,25 +283,18 @@ void JvmtiBreakpoints::clearall_in_class_at_safepoint(Klass* klass) {
 //
 
 JvmtiBreakpoints *JvmtiCurrentBreakpoints::_jvmti_breakpoints  = nullptr;
-address *         JvmtiCurrentBreakpoints::_breakpoint_list    = nullptr;
-
 
 JvmtiBreakpoints& JvmtiCurrentBreakpoints::get_jvmti_breakpoints() {
-  if (_jvmti_breakpoints != nullptr) return (*_jvmti_breakpoints);
-  _jvmti_breakpoints = new JvmtiBreakpoints(listener_fun);
-  assert(_jvmti_breakpoints != nullptr, "_jvmti_breakpoints != nullptr");
+  if (_jvmti_breakpoints == nullptr) {
+    JvmtiBreakpoints* breakpoints = new JvmtiBreakpoints();
+    if (!Atomic::replace_if_null(&_jvmti_breakpoints, breakpoints)) {
+      // already created concurently
+      delete breakpoints;
+    }
+  }
   return (*_jvmti_breakpoints);
 }
 
-void  JvmtiCurrentBreakpoints::listener_fun(void *this_obj, address *cache) {
-  JvmtiBreakpoints *this_jvmti = (JvmtiBreakpoints *) this_obj;
-  assert(this_jvmti != nullptr, "this_jvmti != nullptr");
-
-  debug_only(int n = this_jvmti->length(););
-  assert(cache[n] == nullptr, "cache must be null terminated");
-
-  set_breakpoint_list(cache);
-}
 
 ///////////////////////////////////////////////////////////////
 //
diff --git a/src/hotspot/share/prims/jvmtiImpl.hpp b/src/hotspot/share/prims/jvmtiImpl.hpp
index 5977e000621..3a103cc8237 100644
--- a/src/hotspot/share/prims/jvmtiImpl.hpp
+++ b/src/hotspot/share/prims/jvmtiImpl.hpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2024, Oracle and/or its affiliates. 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
@@ -36,240 +36,102 @@
 #include "runtime/vmOperations.hpp"
 #include "utilities/ostream.hpp"
 
-//
-// Forward Declarations
-//
-
-class JvmtiBreakpoint;
-class JvmtiBreakpoints;
-
-
-///////////////////////////////////////////////////////////////
-//
-// class GrowableCache, GrowableElement
-// Used by              : JvmtiBreakpointCache
-// Used by JVMTI methods: none directly.
-//
-// GrowableCache is a permanent CHeap growable array of <GrowableElement *>
-//
-// In addition, the GrowableCache maintains a null terminated cache array of type address
-// that's created from the element array using the function:
-//     address GrowableElement::getCacheValue().
-//
-// Whenever the GrowableArray changes size, the cache array gets recomputed into a new C_HEAP allocated
-// block of memory. Additionally, every time the cache changes its position in memory, the
-//    void (*_listener_fun)(void *this_obj, address* cache)
-// gets called with the cache's new address. This gives the user of the GrowableCache a callback
-// to update its pointer to the address cache.
-//
-
-class GrowableElement : public CHeapObj<mtInternal> {
-public:
-  virtual ~GrowableElement() {}
-  virtual address getCacheValue()                     =0;
-  virtual bool equals(const GrowableElement* e) const =0;
-  virtual GrowableElement* clone()                    =0;
-};
-
-class GrowableCache {
-
-private:
-  // Object pointer passed into cache & listener functions.
-  void *_this_obj;
-
-  // Array of elements in the collection
-  GrowableArray<GrowableElement *> *_elements;
-
-  // Parallel array of cached values
-  address *_cache;
-
-  // Listener for changes to the _cache field.
-  // Called whenever the _cache field has it's value changed
-  // (but NOT when cached elements are recomputed).
-  void (*_listener_fun)(void *, address*);
-
-  // recache all elements after size change, notify listener
-  void recache();
-
-public:
-   GrowableCache();
-   ~GrowableCache();
-
-  void initialize(void *this_obj, void listener_fun(void *, address*) );
-
-  // number of elements in the collection
-  int length();
-  // get the value of the index element in the collection
-  GrowableElement* at(int index);
-  // find the index of the element, -1 if it doesn't exist
-  int find(const GrowableElement* e) const;
-  // append a copy of the element to the end of the collection, notify listener
-  void append(GrowableElement* e);
-  // remove the element at index, notify listener
-  void remove (int index);
-  // clear out all elements and release all heap space, notify listener
-  void clear();
-};
-
-
-///////////////////////////////////////////////////////////////
-//
-// class JvmtiBreakpointCache
-// Used by              : JvmtiBreakpoints
-// Used by JVMTI methods: none directly.
-// Note   : typesafe wrapper for GrowableCache of JvmtiBreakpoint
-//
-
-class JvmtiBreakpointCache : public CHeapObj<mtInternal> {
-
-private:
-  GrowableCache _cache;
-
-public:
-  JvmtiBreakpointCache()  {}
-  ~JvmtiBreakpointCache() {}
-
-  void initialize(void *this_obj, void listener_fun(void *, address*) ) {
-    _cache.initialize(this_obj, listener_fun);
-  }
-
-  int length()                          { return _cache.length(); }
-  JvmtiBreakpoint& at(int index)        { return (JvmtiBreakpoint&) *(_cache.at(index)); }
-  int find(JvmtiBreakpoint& e)          { return _cache.find((GrowableElement *) &e); }
-  void append(JvmtiBreakpoint& e)       { _cache.append((GrowableElement *) &e); }
-  void remove (int index)               { _cache.remove(index); }
-};
-
 
 ///////////////////////////////////////////////////////////////
 //
 // class JvmtiBreakpoint
-// Used by              : JvmtiBreakpoints
-// Used by JVMTI methods: SetBreakpoint, ClearBreakpoint, ClearAllBreakpoints
-// Note: Extends GrowableElement for use in a GrowableCache
 //
 // A JvmtiBreakpoint describes a location (class, method, bci) to break at.
 //
 
 typedef void (Method::*method_action)(int _bci);
 
-class JvmtiBreakpoint : public GrowableElement {
+class JvmtiBreakpoint : public CHeapObj<mtInternal> {
 private:
   Method*               _method;
   int                   _bci;
   OopHandle             _class_holder;  // keeps _method memory from being deallocated
 
 public:
-  JvmtiBreakpoint() : _method(nullptr), _bci(0) {}
   JvmtiBreakpoint(Method* m_method, jlocation location);
+  JvmtiBreakpoint(const JvmtiBreakpoint& bp);
   virtual ~JvmtiBreakpoint();
   bool equals(const JvmtiBreakpoint& bp) const;
-  void copy(JvmtiBreakpoint& bp);
   address getBcp() const;
   void each_method_version_do(method_action meth_act);
   void set();
   void clear();
   void print_on(outputStream* out) const;
 
-  Method* method() { return _method; }
-
-  // GrowableElement implementation
-  address getCacheValue()         { return getBcp(); }
-  bool equals(const GrowableElement* e) const { return equals((const JvmtiBreakpoint&) *e); }
-
-  GrowableElement *clone()        {
-    JvmtiBreakpoint *bp = new JvmtiBreakpoint();
-    bp->copy(*this);
-    return bp;
-  }
+  Method* method() const { return _method; }
 };
 
-
 ///////////////////////////////////////////////////////////////
 //
 // class JvmtiBreakpoints
-// Used by              : JvmtiCurrentBreakpoints
-// Used by JVMTI methods: none directly
-// Note: A Helper class
 //
-// JvmtiBreakpoints is a GrowableCache of JvmtiBreakpoint.
-// All changes to the GrowableCache occur at a safepoint using VM_ChangeBreakpoints.
-//
-// Because _bps is only modified at safepoints, its possible to always use the
-// cached byte code pointers from _bps without doing any synchronization (see JvmtiCurrentBreakpoints).
-//
-// It would be possible to make JvmtiBreakpoints a static class, but I've made it
-// CHeap allocated to emphasize its similarity to JvmtiFramePops.
+// Contains growable array of JvmtiBreakpoint.
+// All changes to the array occur at a safepoint.
 //
 
 class JvmtiBreakpoints : public CHeapObj<mtInternal> {
 private:
+  GrowableArray<JvmtiBreakpoint*> _elements;
 
-  JvmtiBreakpointCache _bps;
+  int length() { return _elements.length(); }
+  JvmtiBreakpoint& at(int index) { return *_elements.at(index); }
+  int find(JvmtiBreakpoint& e) {
+    return _elements.find_if([&](const JvmtiBreakpoint * other_e) { return e.equals(*other_e); });
+  }
+  void append(JvmtiBreakpoint& e) {
+    JvmtiBreakpoint* new_e = new JvmtiBreakpoint(e);
+    _elements.append(new_e);
+  }
+  void remove(int index) {
+    JvmtiBreakpoint* e = _elements.at(index);
+    assert(e != nullptr, "e != nullptr");
+    _elements.remove_at(index);
+    delete e;
+  }
 
-  // These should only be used by VM_ChangeBreakpoints
-  // to insure they only occur at safepoints.
-  // Todo: add checks for safepoint
-  friend class VM_ChangeBreakpoints;
-  void set_at_safepoint(JvmtiBreakpoint& bp);
-  void clear_at_safepoint(JvmtiBreakpoint& bp);
+  friend class JvmtiCurrentBreakpoints;
+  JvmtiBreakpoints(); // accessible only for JvmtiCurrentBreakpoints
 
 public:
-  JvmtiBreakpoints(void listener_fun(void *, address *));
   ~JvmtiBreakpoints();
 
-  int length();
   void print();
 
   int  set(JvmtiBreakpoint& bp);
   int  clear(JvmtiBreakpoint& bp);
+
+  // used by VM_ChangeBreakpoints
+  void set_at_safepoint(JvmtiBreakpoint& bp);
+  void clear_at_safepoint(JvmtiBreakpoint& bp);
+  // used by VM_RedefineClasses
   void clearall_in_class_at_safepoint(Klass* klass);
 };
 
-
 ///////////////////////////////////////////////////////////////
 //
 // class JvmtiCurrentBreakpoints
 //
-// A static wrapper class for the JvmtiBreakpoints that provides:
-// 1. a fast inlined function to check if a byte code pointer is a breakpoint (is_breakpoint).
-// 2. a function for lazily creating the JvmtiBreakpoints class (this is not strictly necessary,
-//    but I'm copying the code from JvmtiThreadState which needs to lazily initialize
-//    JvmtiFramePops).
-// 3. An oops_do entry point for GC'ing the breakpoint array.
+// A static wrapper class for the JvmtiBreakpoints that provides
+// a function for lazily creating the JvmtiBreakpoints class.
 //
 
 class JvmtiCurrentBreakpoints : public AllStatic {
-
 private:
-
   // Current breakpoints, lazily initialized by get_jvmti_breakpoints();
   static JvmtiBreakpoints *_jvmti_breakpoints;
 
-  // null terminated cache of byte-code pointers corresponding to current breakpoints.
-  // Updated only at safepoints (with listener_fun) when the cache is moved.
-  // It exists only to make is_breakpoint fast.
-  static address          *_breakpoint_list;
-  static inline void set_breakpoint_list(address *breakpoint_list) { _breakpoint_list = breakpoint_list; }
-
-  // Listener for the GrowableCache in _jvmti_breakpoints, updates _breakpoint_list.
-  static void listener_fun(void *this_obj, address *cache);
-
 public:
-  static void initialize();
-  static void destroy();
-
-  // lazily create _jvmti_breakpoints and _breakpoint_list
+  // lazily create _jvmti_breakpoints
   static JvmtiBreakpoints& get_jvmti_breakpoints();
 };
 
 ///////////////////////////////////////////////////////////////
 //
-// class VM_ChangeBreakpoints
-// Used by              : JvmtiBreakpoints
-// Used by JVMTI methods: none directly.
-// Note: A Helper class.
-//
 // VM_ChangeBreakpoints implements a VM_Operation for ALL modifications to the JvmtiBreakpoints class.
 //