diff a/src/hotspot/share/runtime/continuation.cpp b/src/hotspot/share/runtime/continuation.cpp
--- a/src/hotspot/share/runtime/continuation.cpp
+++ b/src/hotspot/share/runtime/continuation.cpp
@@ -24,17 +24,20 @@
 
 #include "precompiled.hpp"
 #include "classfile/vmSymbols.hpp"
 #include "gc/shared/barrierSetNMethod.hpp"
 #include "oops/method.inline.hpp"
+#include "oops/oop.inline.hpp"
+#include "prims/jvmtiThreadState.inline.hpp"
 #include "runtime/continuation.hpp"
 #include "runtime/continuationEntry.inline.hpp"
 #include "runtime/continuationHelper.inline.hpp"
 #include "runtime/continuationJavaClasses.inline.hpp"
 #include "runtime/continuationWrapper.inline.hpp"
 #include "runtime/interfaceSupport.inline.hpp"
 #include "runtime/javaThread.inline.hpp"
+#include "runtime/jniHandles.inline.hpp"
 #include "runtime/osThread.hpp"
 #include "runtime/vframe.inline.hpp"
 #include "runtime/vframe_hp.hpp"
 
 // defined in continuationFreezeThaw.cpp
@@ -52,10 +55,126 @@
      THROW_MSG(vmSymbols::java_lang_IllegalStateException(), "pin underflow");
   }
 }
 JVM_END
 
+#if INCLUDE_JVMTI
+class JvmtiUnmountBeginMark : public StackObj {
+  JavaThread* _target;
+  int _preempt_result;
+
+ public:
+  JvmtiUnmountBeginMark(JavaThread* t) :
+    _target(t), _preempt_result(freeze_pinned_native) {
+    assert(!_target->is_in_any_VTMS_transition(), "must be");
+
+    if (JvmtiVTMSTransitionDisabler::VTMS_notify_jvmti_events()) {
+      JvmtiVTMSTransitionDisabler::start_VTMS_transition(JNIHandles::make_local(_target->vthread()), /* is_mount */ false);
+    } else {
+      _target->set_is_in_VTMS_transition(true);
+      java_lang_Thread::set_is_in_VTMS_transition(_target->vthread(), true);
+    }
+  }
+  ~JvmtiUnmountBeginMark() {
+    assert(!_target->is_suspended(), "must be");
+
+    assert(_target->is_in_VTMS_transition(), "must be");
+    assert(java_lang_Thread::is_in_VTMS_transition(_target->vthread()), "must be");
+
+    // Read it again since for late binding agents the flag could have
+    // been set while blocked in the allocation path during freeze.
+    bool jvmti_present = JvmtiVTMSTransitionDisabler::VTMS_notify_jvmti_events();
+
+    if (_preempt_result != freeze_ok) {
+      // Undo transition
+      if (jvmti_present) {
+        JvmtiVTMSTransitionDisabler::finish_VTMS_transition(JNIHandles::make_local(_target->vthread()), false);
+      } else {
+        _target->set_is_in_VTMS_transition(false);
+        java_lang_Thread::set_is_in_VTMS_transition(_target->vthread(), false);
+      }
+    } else {
+      if (jvmti_present) {
+        _target->rebind_to_jvmti_thread_state_of(_target->threadObj());
+        _target->set_jvmti_unmount_event_pending(true);
+      }
+    }
+  }
+  void set_preempt_result(int res) { _preempt_result = res; }
+};
+
+static bool is_safe_vthread_to_preempt_for_jvmti(JavaThread* target, oop vthread) {
+  assert(!target->has_pending_popframe(), "should be true; no support for vthreads yet");
+  JvmtiThreadState* state = target->jvmti_thread_state();
+  assert(state == nullptr || !state->is_earlyret_pending(), "should be true; no support for vthreads yet");
+
+  if (target->is_in_any_VTMS_transition()) {
+    // We caught target at the end of a mount transition (is_in_VTMS_transition()) or at the
+    // beginning or end of a temporary switch to carrier thread (is_in_tmp_VTMS_transition()).
+    return false;
+  }
+  return true;
+}
+#endif
+
+static bool is_safe_vthread_to_preempt(JavaThread* target, oop vthread) {
+  if (!java_lang_VirtualThread::is_instance(vthread) ||                               // inside transition
+      java_lang_VirtualThread::state(vthread) != java_lang_VirtualThread::RUNNING) {  // inside transition
+    return false;
+  }
+  return JVMTI_ONLY(is_safe_vthread_to_preempt_for_jvmti(target, vthread)) NOT_JVMTI(true);
+}
+
+typedef int (*FreezeContFnT)(JavaThread*, intptr_t*);
+
+int Continuation::try_preempt(JavaThread* target, oop continuation, bool set_state_yielding) {
+  assert(target->is_on_monitorenter(), "must be");
+  assert(target->has_last_Java_frame(), "must be");
+  assert(!target->preempting(), "must be");
+  assert(!target->is_suspended() || target->is_disable_suspend(), "must be");
+  assert(!target->has_pending_exception(), "must be");
+  assert(target->last_continuation() != nullptr, "must be");
+  assert(target->last_continuation()->cont_oop(target) == continuation, "must be");
+  assert(!is_continuation_preempted(continuation), "shouldn't be");
+  assert(Continuation::continuation_scope(continuation) == java_lang_VirtualThread::vthread_scope(), "must be");
+
+  if (!VM_Version::supports_cont_preemption()) {
+    return unsupported;
+  }
+
+  if (is_continuation_done(continuation)) {
+    return freeze_not_mounted;
+  }
+
+  // Continuation is mounted and it's not done so check if it's safe to preempt.
+  if (!is_safe_vthread_to_preempt(target, target->vthread())) {
+    return freeze_pinned_native;
+  }
+
+#if INCLUDE_JVMTI
+  JvmtiUnmountBeginMark jubm(target);
+#endif
+  target->set_preempting(true);
+  int res = CAST_TO_FN_PTR(FreezeContFnT, freeze_preempt_entry())(target, target->last_Java_sp());
+  log_trace(continuations, preempt)("try_preempt: %d", res);
+  JVMTI_ONLY(jubm.set_preempt_result(res);)
+  if (res != freeze_ok) {
+    target->set_preempting(false);
+  } else if (set_state_yielding) {
+    java_lang_VirtualThread::set_state(target->vthread(), java_lang_VirtualThread::YIELDING);
+  }
+  return res;
+}
+
+bool Continuation::is_continuation_preempted(oop cont) {
+  return jdk_internal_vm_Continuation::is_preempted(cont);
+}
+
+bool Continuation::is_continuation_done(oop cont) {
+  return jdk_internal_vm_Continuation::done(cont);
+}
+
 #ifndef PRODUCT
 static jlong java_tid(JavaThread* thread) {
   return java_lang_Thread::thread_id(thread->threadObj());
 }
 #endif
