diff a/src/hotspot/cpu/aarch64/gc/shenandoah/shenandoahBarrierSetAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/gc/shenandoah/shenandoahBarrierSetAssembler_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/gc/shenandoah/shenandoahBarrierSetAssembler_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/gc/shenandoah/shenandoahBarrierSetAssembler_aarch64.cpp
@@ -701,22 +701,87 @@
 }
 
 void ShenandoahBarrierStubC2::enter_if_gc_state(MacroAssembler& masm, const char test_state) {
   Assembler::InlineSkippedInstructionsCounter skip_counter(&masm);
 
-  int bit_to_check = ShenandoahThreadLocalData::gc_state_to_fast_bit(test_state);
-  Address gc_state_fast(rthread, in_bytes(ShenandoahThreadLocalData::gc_state_fast_offset()));
-  __ ldrb(rscratch1, gc_state_fast);
-  if (_test_and_branch_reachable) {
-    __ tbnz(rscratch1, bit_to_check, _test_and_branch_reachable_entry);
-  } else {
-    __ tbz(rscratch1, bit_to_check, *continuation());
+  if (ShenandoahGCStateCheckRemove) {
+    // Unrealistic: remove all barrier fastpath checks.
+  } else if (ShenandoahGCStateCheckHotpatch) {
+    // Emit the unconditional branch in the first version of the method.
+    // Let the rest of runtime figure out how to manage it.
+    __ relocate(barrier_Relocation::spec());
     __ b(*entry());
+
+#ifdef ASSERT
+    Address gc_state_fast(rthread, in_bytes(ShenandoahThreadLocalData::gc_state_fast_offset()));
+    __ ldrb(rscratch1, gc_state_fast);
+    __ cbz(rscratch1, *continuation());
+    __ hlt(0); // Correctness bug: barrier is NOP-ed, but heap is NOT IDLE
+#endif
+    __ bind(*continuation());
+  } else {
+    int bit_to_check = ShenandoahThreadLocalData::gc_state_to_fast_bit(test_state);
+    Address gc_state_fast(rthread, in_bytes(ShenandoahThreadLocalData::gc_state_fast_offset()));
+    __ ldrb(rscratch1, gc_state_fast);
+    if (_test_and_branch_reachable) {
+      __ tbnz(rscratch1, bit_to_check, _test_and_branch_reachable_entry);
+    } else {
+      __ tbz(rscratch1, bit_to_check, *continuation());
+      __ b(*entry());
+    }
+
+    // This is were the slowpath stub will return to or the code above will
+    // jump to if the checks are false
+    __ bind(*continuation());
+  }
+}
+
+address ShenandoahBarrierSetAssembler::parse_stub_address(address pc) {
+  NativeInstruction* ni = nativeInstruction_at(pc);
+  assert(ni->is_jump(), "Initial code version: GC barrier fastpath must be a jump");
+  NativeJump* jmp = nativeJump_at(pc);
+  return jmp->jump_destination();
+}
+
+void insert_nop(address pc) {
+  *(pc + 0) = 0x1F;
+  *(pc + 1) = 0x20;
+  *(pc + 2) = 0x03;
+  *(pc + 3) = 0xD5;
+  ICache::invalidate_range(pc, 4);
+}
+
+bool is_nop(address pc) {
+  if (*(pc + 0) != 0x1F) return false;
+  if (*(pc + 1) != 0x20) return false;
+  if (*(pc + 2) != 0x03) return false;
+  if (*(pc + 3) != 0xD5) return false;
+  return true;
+}
+
+void check_at(bool cond, address pc, const char* msg) {
+  assert(cond, "%s: at PC " PTR_FORMAT ": %02x%02x%02x%02x%02x",
+         msg, p2i(pc), *(pc + 0), *(pc + 1), *(pc + 2), *(pc + 3), *(pc + 4));
+}
+
+void ShenandoahBarrierSetAssembler::patch_branch_to_nop(address pc) {
+  NativeInstruction* ni = nativeInstruction_at(pc);
+  if (ni->is_jump()) {
+    insert_nop(pc);
+  } else {
+    check_at(is_nop(pc), pc, "Should already be nop");
+  }
+}
+
+void ShenandoahBarrierSetAssembler::patch_nop_to_branch(address pc, address stub_addr) {
+  NativeInstruction* ni = nativeInstruction_at(pc);
+  if (is_nop(pc)) {
+    NativeJump::insert(pc, stub_addr);
+  } else {
+    check_at(ni->is_jump(), pc, "Should already be jump");
+    check_at(nativeJump_at(pc)->jump_destination() == stub_addr, pc, "Jump should be to the same address");
   }
-  // This is were the slowpath stub will return to or the code above will
-  // jump to if the checks are false
-  __ bind(*continuation());
 }
 
 bool needs_acquiring_load_exclusive(const MachNode *n) {
   assert(n->is_CAS(true), "expecting a compare and swap");
   if (n->is_CAS(false)) {
@@ -875,11 +940,11 @@
     stub->enter_if_gc_state(*masm, check);
   }
 }
 
 void ShenandoahBarrierSetAssembler::card_barrier_c2(const MachNode* node, MacroAssembler* masm, Address address) {
-  if (!ShenandoahBarrierStubC2::needs_card_barrier(node)) {
+  if (ShenandoahSkipBarriers || (node->barrier_data() & ShenandoahBitCardMark) == 0) {
     return;
   }
 
   assert(CardTable::dirty_card_val() == 0, "must be");
   Assembler::InlineSkippedInstructionsCounter skip_counter(masm);
@@ -942,11 +1007,11 @@
 }
 
 void ShenandoahBarrierStubC2::emit_code(MacroAssembler& masm) {
   // If we reach here with _skip_trampoline set it means that earlier we
   // emitted a trampoline to this stub and now we need to emit the actual stub.
-  if (_skip_trampoline) {
+  if (ShenandoahGCStateCheckHotpatch || _skip_trampoline) {
     emit_code_actual(masm);
   } else {
     _skip_trampoline = true;
 
     // The fastpath executes two branch instructions to reach this stub, let's
@@ -1074,11 +1139,12 @@
   if (!_needs_keep_alive_barrier) {
     return ;
   }
 
   // If another barrier is enabled as well, do a runtime check for a specific barrier.
-  if (_needs_load_ref_barrier) {
+  // Hotpatched GC checks only care about idle/non-idle state, so needs a check anyhow.
+  if (_needs_load_ref_barrier || ShenandoahGCStateCheckHotpatch) {
     Address gcs_addr(rthread, in_bytes(ShenandoahThreadLocalData::gc_state_offset()));
     __ ldrb(tmp1, gcs_addr);
     __ tbz(tmp1, ShenandoahHeap::MARKING_BITPOS, L_done);
   }
 
@@ -1124,11 +1190,12 @@
     return ;
   }
 
   if ((_node->barrier_data() & ShenandoahBitStrong) != 0) {
     // If another barrier is enabled as well, do a runtime check for a specific barrier.
-    if (_needs_keep_alive_barrier) {
+    // Hotpatched GC checks only care about idle/non-idle state, so needs a check anyhow.
+    if (_needs_keep_alive_barrier || ShenandoahGCStateCheckHotpatch) {
       char state_to_check = ShenandoahHeap::HAS_FORWARDED | (_needs_load_ref_weak_barrier ? ShenandoahHeap::WEAK_ROOTS : 0);
       int bit_to_check = ShenandoahThreadLocalData::gc_state_to_fast_bit(state_to_check);
       Address gc_state_fast(rthread, in_bytes(ShenandoahThreadLocalData::gc_state_fast_offset()));
       __ ldrb(tmp, gc_state_fast);
       __ tbz(tmp, bit_to_check, L_done);
