diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/CallFrame.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/CallFrame.java
index 0ab3768c7eda..ca8a82fa21ad 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/CallFrame.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/CallFrame.java
@@ -42,7 +42,7 @@ public final class CallFrame {
 
     private final byte typeTag;
     private final long classId;
-    private final MethodRef method;
+    private final MethodVersionRef methodVersion;
     private final long methodId;
     private final long codeIndex;
     private final long threadId;
@@ -55,14 +55,14 @@ public final class CallFrame {
     private Object scope;
     private final TruffleLogger logger;
 
-    public CallFrame(long threadId, byte typeTag, long classId, MethodRef method, long methodId, long codeIndex, Frame frame, Node currentNode, RootNode rootNode,
+    public CallFrame(long threadId, byte typeTag, long classId, MethodVersionRef methodVersion, long methodId, long codeIndex, Frame frame, Node currentNode, RootNode rootNode,
                     DebugStackFrame debugStackFrame, JDWPContext context, TruffleLogger logger) {
         this.threadId = threadId;
         this.typeTag = typeTag;
         this.classId = classId;
-        this.method = method;
+        this.methodVersion = methodVersion;
         this.methodId = methodId;
-        this.codeIndex = method != null && method.isObsolete() ? -1 : codeIndex;
+        this.codeIndex = methodVersion != null && methodVersion.isObsolete() ? -1 : codeIndex;
         this.frame = frame;
         this.currentNode = currentNode;
         this.rootNode = rootNode;
@@ -85,15 +85,22 @@ public long getClassId() {
         return classId;
     }
 
+    public MethodVersionRef getMethodVersion() {
+        return methodVersion;
+    }
+
     public MethodRef getMethod() {
-        return method;
+        if (methodVersion != null) {
+            return methodVersion.getMethod();
+        }
+        return null;
     }
 
     public long getMethodId() {
-        if (method == null) {
+        if (methodVersion == null) {
             return methodId;
         }
-        return method.isObsolete() ? 0 : methodId;
+        return methodVersion.isObsolete() ? 0 : methodId;
     }
 
     public long getCodeIndex() {
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/Ids.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/Ids.java
index 664cd2f9e9da..24dfc0441f3c 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/Ids.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/Ids.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2025, 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
@@ -172,31 +172,6 @@ private synchronized long generateUniqueId(T object) {
         return id;
     }
 
-    public void replaceObject(T original, T replacement) {
-        int id = (int) getIdAsLong(original);
-        objects[id] = new WeakReference<>(replacement);
-        log(() -> "Replaced ID: " + id);
-    }
-
-    @SuppressWarnings({"unchecked", "rawtypes"})
-    public void updateId(KlassRef klass) {
-        // remove existing ID
-        removeId(klass);
-        Long theId = innerClassIDMap.get(klass.getNameAsString());
-        if (theId != null) {
-            // then inject klass under the new ID
-            objects[(int) (long) theId] = new WeakReference(klass);
-        }
-    }
-
-    @SuppressWarnings({"unchecked", "rawtypes"})
-    private void removeId(KlassRef klass) {
-        int id = (int) getId(klass);
-        if (id > 0) {
-            objects[id] = new WeakReference<>(null);
-        }
-    }
-
     public boolean checkRemoved(long refTypeId) {
         return innerClassIDMap.containsValue(refTypeId);
     }
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/JDWPContext.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/JDWPContext.java
index a69260c15242..18a6103cc09b 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/JDWPContext.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/JDWPContext.java
@@ -73,7 +73,7 @@ public interface JDWPContext {
      * @param root the Truffle root node object
      * @return the declaring method of the root node
      */
-    MethodRef getMethodFromRootNode(RootNode root);
+    MethodVersionRef getMethodFromRootNode(RootNode root);
 
     /**
      * @return guest language array of all active threads
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodRef.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodRef.java
index 92784b3f0675..986236e98b2c 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodRef.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodRef.java
@@ -175,14 +175,6 @@ public interface MethodRef {
      */
     boolean hasSourceFileAttribute();
 
-    /**
-     * Determines if the code index is located in the source file on the last line of this method.
-     *
-     * @param codeIndex
-     * @return true if last line, false otherwise
-     */
-    boolean isLastLine(long codeIndex);
-
     /**
      * Returns the klass that declares this method.
      *
@@ -234,16 +226,6 @@ public interface MethodRef {
 
     void disposeHooks();
 
-    /**
-     * Determine if this method is obsolete. A method is obsolete if it has been replaced by a
-     * non-equivalent method using the RedefineClasses command. The original and redefined methods
-     * are considered equivalent if their bytecodes are the same except for indices into the
-     * constant pool and the referenced constants are equal.
-     *
-     * @return true if the method is obsolete
-     */
-    boolean isObsolete();
-
     /**
      * Returns the last bci of the method.
      *
@@ -264,4 +246,11 @@ public interface MethodRef {
      * @return true if the method is a static initializer
      */
     boolean isClassInitializer();
+
+    /**
+     * Determines if this method was removed by redefinition.
+     *
+     * @return true if removed
+     */
+    boolean isRemovedByRedefinition();
 }
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodVersionRef.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodVersionRef.java
new file mode 100644
index 000000000000..180570b093c1
--- /dev/null
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/MethodVersionRef.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025, 2025, 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
+ * 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.
+ */
+package com.oracle.truffle.espresso.jdwp.api;
+
+public interface MethodVersionRef {
+    /**
+     * Returns the MethodRef corresponding to this method version.
+     *
+     * @return the MethodRef
+     */
+    MethodRef getMethod();
+
+    /**
+     * Determine if this method is obsolete. A method is obsolete if it has been replaced by a
+     * non-equivalent method using the RedefineClasses command. The original and redefined methods
+     * are considered equivalent if their bytecodes are the same except for indices into the
+     * constant pool and the referenced constants are equal.
+     *
+     * @return true if the method is obsolete
+     */
+    boolean isObsolete();
+}
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/VMListener.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/VMListener.java
index 5adc987ad0ba..edd7a8017446 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/VMListener.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/api/VMListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2029, 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
@@ -96,7 +96,7 @@ public interface VMListener {
      * This method should be called when when the monitor wait(timeout) method is invoked in the
      * guest VM. A monitor wait event will then be sent through JDWP, if there was a request for the
      * current thread.
-     * 
+     *
      * @param monitor the monitor object
      * @param timeout the timeout in ms before the wait will time out
      */
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/DebuggerController.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/DebuggerController.java
index 66a228bf0187..efe8d39aee49 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/DebuggerController.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/DebuggerController.java
@@ -68,6 +68,7 @@
 import com.oracle.truffle.espresso.jdwp.api.JDWPOptions;
 import com.oracle.truffle.espresso.jdwp.api.KlassRef;
 import com.oracle.truffle.espresso.jdwp.api.MethodRef;
+import com.oracle.truffle.espresso.jdwp.api.MethodVersionRef;
 import com.oracle.truffle.espresso.jdwp.api.VMEventListener;
 
 public final class DebuggerController implements ContextsListener {
@@ -415,10 +416,10 @@ private void doStepOut(SuspendedInfo susp) {
         SteppingInfo steppingInfo = commandRequestIds.get(susp.getThread());
         if (steppingInfo != null && stepOutBCI != -1) {
             // record the location that we'll land on after the step out completes
-            MethodRef method = context.getMethodFromRootNode(callerRoot);
+            MethodVersionRef method = context.getMethodFromRootNode(callerRoot);
             if (method != null) {
-                KlassRef klass = method.getDeclaringKlassRef();
-                steppingInfo.setStepOutBCI(context.getIds().getIdAsLong(klass), context.getIds().getIdAsLong(method), stepOutBCI);
+                KlassRef klass = method.getMethod().getDeclaringKlassRef();
+                steppingInfo.setStepOutBCI(context.getIds().getIdAsLong(klass), context.getIds().getIdAsLong(method.getMethod()), stepOutBCI);
             }
         }
     }
@@ -869,16 +870,17 @@ public CallFrame[] getCallFrames(Object guestThread) {
         List<CallFrame> callFrames = new ArrayList<>();
         Truffle.getRuntime().iterateFrames(frameInstance -> {
             KlassRef klass;
-            MethodRef method;
+            MethodVersionRef methodVersion;
             RootNode root = getRootNode(frameInstance);
             if (root == null) {
                 return null;
             }
-            method = getContext().getMethodFromRootNode(root);
-            if (method == null) {
+            methodVersion = getContext().getMethodFromRootNode(root);
+            if (methodVersion == null) {
                 return null;
             }
 
+            MethodRef method = methodVersion.getMethod();
             klass = method.getDeclaringKlassRef();
             long klassId = ids.getIdAsLong(klass);
             long methodId = ids.getIdAsLong(method);
@@ -914,7 +916,7 @@ public CallFrame[] getCallFrames(Object guestThread) {
             if (currentNode instanceof RootNode) {
                 currentNode = context.getInstrumentableNode((RootNode) currentNode);
             }
-            callFrames.add(new CallFrame(context.getIds().getIdAsLong(guestThread), typeTag, klassId, method, methodId, codeIndex, frame, currentNode, root, null, context, jdwpLogger));
+            callFrames.add(new CallFrame(context.getIds().getIdAsLong(guestThread), typeTag, klassId, methodVersion, methodId, codeIndex, frame, currentNode, root, null, context, jdwpLogger));
             return null;
         });
         return callFrames.toArray(new CallFrame[0]);
@@ -1227,11 +1229,11 @@ private CallFrame[] createCallFrames(long threadId, Iterable<DebugStackFrame> st
                 }
 
                 Frame rawFrame = frame.getRawFrame(context.getLanguageClass(), FrameInstance.FrameAccess.READ_WRITE);
-                MethodRef method = context.getMethodFromRootNode(root);
-                KlassRef klass = method.getDeclaringKlassRef();
+                MethodVersionRef methodVersion = context.getMethodFromRootNode(root);
+                KlassRef klass = methodVersion.getMethod().getDeclaringKlassRef();
 
                 klassId = ids.getIdAsLong(klass);
-                methodId = ids.getIdAsLong(method);
+                methodId = ids.getIdAsLong(methodVersion.getMethod());
                 typeTag = TypeTag.getKind(klass);
 
                 // check if we have a dedicated step out code index on the top frame
@@ -1241,7 +1243,7 @@ private CallFrame[] createCallFrames(long threadId, Iterable<DebugStackFrame> st
                     codeIndex = context.getBCI(rawNode, rawFrame);
                 }
 
-                list.addLast(new CallFrame(threadId, typeTag, klassId, method, methodId, codeIndex, rawFrame, rawNode, root, frame, context, jdwpLogger));
+                list.addLast(new CallFrame(threadId, typeTag, klassId, methodVersion, methodId, codeIndex, rawFrame, rawNode, root, frame, context, jdwpLogger));
                 frameCount++;
                 if (frameLimit != -1 && frameCount >= frameLimit) {
                     return list.toArray(new CallFrame[0]);
diff --git a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/JDWP.java b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/JDWP.java
index 05b8bbc01799..6c822d309410 100644
--- a/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/JDWP.java
+++ b/espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/JDWP.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2025, 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
@@ -1518,7 +1518,8 @@ static CommandResult createReply(Packet packet, JDWPContext context) {
                     if (method == null) {
                         return new CommandResult(reply);
                     }
-                    reply.writeBoolean(method.isObsolete());
+                    // only condition to check here is if removed by redefinition
+                    reply.writeBoolean(method.isRemovedByRedefinition());
                 }
                 return new CommandResult(reply);
             }
@@ -2154,7 +2155,7 @@ static CommandResult createReply(Packet packet, DebuggerController controller) {
                     reply.writeLong(controller.getContext().getIds().getIdAsLong(frame));
                     reply.writeByte(frame.getTypeTag());
                     reply.writeLong(frame.getClassId());
-                    reply.writeLong(frame.getMethod().isObsolete() ? 0 : controller.getContext().getIds().getIdAsLong(frame.getMethod()));
+                    reply.writeLong(frame.getMethodVersion().isObsolete() ? 0 : controller.getContext().getIds().getIdAsLong(frame.getMethod()));
                     reply.writeLong(frame.getCodeIndex());
                 }
                 return new CommandResult(reply);
diff --git a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/impl/Method.java b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/impl/Method.java
index 3a5704cc944e..e391615c0db6 100644
--- a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/impl/Method.java
+++ b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/impl/Method.java
@@ -115,6 +115,7 @@
 import com.oracle.truffle.espresso.jdwp.api.KlassRef;
 import com.oracle.truffle.espresso.jdwp.api.MethodHook;
 import com.oracle.truffle.espresso.jdwp.api.MethodRef;
+import com.oracle.truffle.espresso.jdwp.api.MethodVersionRef;
 import com.oracle.truffle.espresso.jni.Mangle;
 import com.oracle.truffle.espresso.meta.EspressoError;
 import com.oracle.truffle.espresso.meta.Meta;
@@ -276,22 +277,6 @@ public Attribute getAttribute(Symbol<Name> attrName) {
         return getLinkedMethod().getAttribute(attrName);
     }
 
-    @Override
-    @TruffleBoundary
-    public int bciToLineNumber(int bci) {
-        if (bci < 0) {
-            return bci;
-        }
-        if (isNative()) {
-            return EspressoStackElement.NATIVE_BCI;
-        }
-        if (getCodeAttribute() == null) {
-            assert isAbstract();
-            return EspressoStackElement.UNKNOWN_BCI;
-        }
-        return getCodeAttribute().bciToLineNumber(bci);
-    }
-
     @Override
     public EspressoContext getContext() {
         return declaringKlass.getContext();
@@ -849,11 +834,6 @@ public Method setPoisonPill() {
         return this;
     }
 
-    @Override
-    public boolean hasSourceFileAttribute() {
-        return declaringKlass.getAttribute(Names.SourceFile) != null;
-    }
-
     public String report(int curBCI) {
         String sourceFile = getDeclaringKlass().getSourceFile();
         if (sourceFile == null) {
@@ -1000,103 +980,6 @@ public String getInteropString() {
         return getNameAsString() + AbstractLookupNode.METHOD_SELECTION_SEPARATOR + getRawSignature();
     }
 
-    @Override
-    public boolean hasActiveHook() {
-        return hasActiveHook.get();
-    }
-
-    @Override
-    public synchronized MethodHook[] getMethodHooks() {
-        return Arrays.copyOf(hooks, hooks.length);
-    }
-
-    @Override
-    public synchronized void addMethodHook(MethodHook info) {
-        hasActiveHook.set(true);
-        hooks = Arrays.copyOf(hooks, hooks.length + 1);
-        hooks[hooks.length - 1] = info;
-    }
-
-    private void expectActiveHooks() {
-        if (hooks.length == 0) {
-            throw new RuntimeException("Method: " + getNameAsString() + " expected to contain method hook");
-        }
-    }
-
-    @Override
-    public synchronized void removeMethodHook(int requestId) {
-        expectActiveHooks();
-        boolean removed = false;
-        // shrink the array to avoid null values
-        if (hooks.length == 1) {
-            // make sure it's the right hook
-            if (hooks[0].getRequestId() == requestId) {
-                hooks = MethodHook.EMPTY;
-                hasActiveHook.set(false);
-                removed = true;
-            }
-        } else {
-            int removeIndex = -1;
-            for (int i = 0; i < hooks.length; i++) {
-                if (hooks[i].getRequestId() == requestId) {
-                    removeIndex = i;
-                    break;
-                }
-            }
-            if (removeIndex != -1) {
-                MethodHook[] temp = new MethodHook[hooks.length - 1];
-                for (int i = 0; i < temp.length; i++) {
-                    temp[i] = i < removeIndex ? hooks[i] : hooks[i + 1];
-                }
-                hooks = temp;
-                removed = true;
-            }
-        }
-        if (!removed) {
-            throw new RuntimeException("Method: " + getNameAsString() + " should contain method hook");
-        }
-    }
-
-    @Override
-    public synchronized void removeMethodHook(MethodHook hook) {
-        expectActiveHooks();
-        boolean removed = false;
-        // shrink the array to avoid null values
-        if (hooks.length == 1) {
-            // make sure it's the right hook
-            if (hooks[0] == hook) {
-                hooks = MethodHook.EMPTY;
-                hasActiveHook.set(false);
-                removed = true;
-            }
-        } else {
-            int removeIndex = -1;
-            for (int i = 0; i < hooks.length; i++) {
-                if (hooks[i] == hook) {
-                    removeIndex = i;
-                    break;
-                }
-            }
-            if (removeIndex != -1) {
-                MethodHook[] temp = new MethodHook[hooks.length - 1];
-                for (int i = 0; i < temp.length; i++) {
-                    temp[i] = i < removeIndex ? hooks[i] : hooks[i + 1];
-                }
-                hooks = temp;
-                removed = true;
-            }
-        }
-        if (!removed) {
-            throw new RuntimeException("Method: " + getNameAsString() + " should contain method hook");
-        }
-    }
-
-    @Override
-    public synchronized void disposeHooks() {
-        hasActiveHook.set(false);
-        hooks = MethodHook.EMPTY;
-    }
-
     SharedRedefinitionContent redefine(ObjectKlass.KlassVersion klassVersion, ParserMethod newMethod, ParserKlass newKlass) {
         // install the new method version immediately
         LinkedMethod newLinkedMethod = new LinkedMethod(newMethod);
@@ -1149,6 +1032,7 @@ public void removedByRedefinition() {
         removedByRedefinition = true;
     }
 
+    @Override
     public boolean isRemovedByRedefinition() {
         return removedByRedefinition;
     }
@@ -1407,11 +1291,6 @@ public String getSourceFile() {
         return getDeclaringKlass().getSourceFile();
     }
 
-    @Override
-    public String getNameAsString() {
-        return getName().toString();
-    }
-
     @Override
     public String getSignatureAsString() {
         return getRawSignature().toString();
@@ -1461,40 +1340,40 @@ public LineNumberTableAttribute getLineNumberTable() {
 
     @Override
     public Object invokeMethodVirtual(Object... args) {
-        getMethodVersion().checkRemovedByRedefinition();
+        checkRemovedByRedefinition();
         return invokeDirectVirtual(args);
     }
 
     @Override
     public Object invokeMethodStatic(Object... args) {
-        getMethodVersion().checkRemovedByRedefinition();
+        checkRemovedByRedefinition();
         return invokeDirectStatic(args);
     }
 
     @Override
     public Object invokeMethodSpecial(Object... args) {
-        getMethodVersion().checkRemovedByRedefinition();
+        checkRemovedByRedefinition();
         return invokeDirectSpecial(args);
     }
 
     @Override
     public Object invokeMethodInterface(Object... args) {
-        getMethodVersion().checkRemovedByRedefinition();
+        checkRemovedByRedefinition();
         return invokeDirectInterface(args);
     }
 
     @Override
     public Object invokeMethodNonVirtual(Object... args) {
-        getMethodVersion().checkRemovedByRedefinition();
+        checkRemovedByRedefinition();
         return invokeDirect(args);
     }
 
-    @Override
-    public boolean isLastLine(long codeIndex) {
-        LineNumberTableAttribute table = getLineNumberTable();
-        int lastLine = table.getLastLine();
-        int lineAt = table.getLineNumber((int) codeIndex);
-        return lastLine == lineAt;
+    private void checkRemovedByRedefinition() {
+        if (isRemovedByRedefinition()) {
+            Meta meta = getMeta();
+            throw meta.throwExceptionWithMessage(meta.java_lang_NoSuchMethodError,
+                            meta.toGuestString(getDeclaringKlass().getNameAsString() + "." + getName() + getRawSignature()));
+        }
     }
 
     @Override
@@ -1507,11 +1386,6 @@ public int getLastLine() {
         return getLineNumberTable().getLastLine();
     }
 
-    @Override
-    public boolean isObsolete() {
-        return !getMethodVersion().klassVersion.getAssumption().isValid();
-    }
-
     @Override
     public long getLastBCI() {
         int bci = 0;
@@ -1529,6 +1403,126 @@ public long getLastBCI() {
         return bci;
     }
 
+    @Override
+    public int bciToLineNumber(int bci) {
+        if (bci < 0) {
+            return bci;
+        }
+        if (isNative()) {
+            return EspressoStackElement.NATIVE_BCI;
+        }
+        if (getCodeAttribute() == null) {
+            assert isAbstract();
+            return EspressoStackElement.UNKNOWN_BCI;
+        }
+        return getCodeAttribute().bciToLineNumber(bci);
+    }
+
+    @Override
+    public boolean hasSourceFileAttribute() {
+        return declaringKlass.getAttribute(Names.SourceFile) != null;
+    }
+
+    @Override
+    public String getNameAsString() {
+        return getName().toString();
+    }
+
+    @Override
+    public boolean hasActiveHook() {
+        return hasActiveHook.get();
+    }
+
+    @Override
+    public synchronized MethodHook[] getMethodHooks() {
+        return Arrays.copyOf(hooks, hooks.length);
+    }
+
+    @Override
+    public synchronized void addMethodHook(MethodHook info) {
+        hasActiveHook.set(true);
+        hooks = Arrays.copyOf(hooks, hooks.length + 1);
+        hooks[hooks.length - 1] = info;
+    }
+
+    private void expectActiveHooks() {
+        if (hooks.length == 0) {
+            throw new RuntimeException("Method: " + getNameAsString() + " expected to contain method hook");
+        }
+    }
+
+    @Override
+    public synchronized void removeMethodHook(int requestId) {
+        expectActiveHooks();
+        boolean removed = false;
+        // shrink the array to avoid null values
+        if (hooks.length == 1) {
+            // make sure it's the right hook
+            if (hooks[0].getRequestId() == requestId) {
+                hooks = MethodHook.EMPTY;
+                hasActiveHook.set(false);
+                removed = true;
+            }
+        } else {
+            int removeIndex = -1;
+            for (int i = 0; i < hooks.length; i++) {
+                if (hooks[i].getRequestId() == requestId) {
+                    removeIndex = i;
+                    break;
+                }
+            }
+            if (removeIndex != -1) {
+                MethodHook[] temp = new MethodHook[hooks.length - 1];
+                for (int i = 0; i < temp.length; i++) {
+                    temp[i] = i < removeIndex ? hooks[i] : hooks[i + 1];
+                }
+                hooks = temp;
+                removed = true;
+            }
+        }
+        if (!removed) {
+            throw new RuntimeException("Method: " + getNameAsString() + " should contain method hook");
+        }
+    }
+
+    @Override
+    public synchronized void removeMethodHook(MethodHook hook) {
+        expectActiveHooks();
+        boolean removed = false;
+        // shrink the array to avoid null values
+        if (hooks.length == 1) {
+            // make sure it's the right hook
+            if (hooks[0] == hook) {
+                hooks = MethodHook.EMPTY;
+                hasActiveHook.set(false);
+                removed = true;
+            }
+        } else {
+            int removeIndex = -1;
+            for (int i = 0; i < hooks.length; i++) {
+                if (hooks[i] == hook) {
+                    removeIndex = i;
+                    break;
+                }
+            }
+            if (removeIndex != -1) {
+                MethodHook[] temp = new MethodHook[hooks.length - 1];
+                for (int i = 0; i < temp.length; i++) {
+                    temp[i] = i < removeIndex ? hooks[i] : hooks[i + 1];
+                }
+                hooks = temp;
+                removed = true;
+            }
+        }
+        if (!removed) {
+            throw new RuntimeException("Method: " + getNameAsString() + " should contain method hook");
+        }
+    }
+
+    public synchronized void disposeHooks() {
+        hasActiveHook.set(false);
+        hooks = MethodHook.EMPTY;
+    }
     // endregion Methodref impl
 
     private static final class Continuum {
@@ -1586,7 +1580,7 @@ public CallTarget getContinuableCallTarget(MethodVersion mv, int bci) {
      * Each time a method is changed via class redefinition it gets a new version, and therefore a
      * new MethodVersion object.
      */
-    public final class MethodVersion implements ModifiersProvider {
+    public final class MethodVersion implements MethodVersionRef, ModifiersProvider {
         private static final int CODE_FLAGS_MASK = 0b00001111;
         private static final int CODE_FLAGS_READY = 0x1;
         private static final int CODE_FLAGS_HAS_JSR = 0x2;
@@ -1712,11 +1706,17 @@ public LinkedMethod getLinkedMethod() {
             return linkedMethod;
         }
 
+        @Override
         @Idempotent
         public Method getMethod() {
             return Method.this;
         }
 
+        @Override
+        public boolean isObsolete() {
+            return !klassVersion.getAssumption().isValid();
+        }
+
         public Symbol<Name> getName() {
             return linkedMethod.getName();
         }
@@ -1977,14 +1977,6 @@ public LineNumberTableAttribute getLineNumberTableAttribute() {
             return LineNumberTableAttribute.EMPTY;
         }
 
-        private void checkRemovedByRedefinition() {
-            if (getMethod().isRemovedByRedefinition()) {
-                Meta meta = getMeta();
-                throw meta.throwExceptionWithMessage(meta.java_lang_NoSuchMethodError,
-                                meta.toGuestString(getMethod().getDeclaringKlass().getNameAsString() + "." + getName() + getRawSignature()));
-            }
-        }
-
         @Override
         public String toString() {
             return "EspressoMethod<" + getDeclaringKlass().getType() + "." + getName() + getRawSignature() + ">";
diff --git a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java
index ce8b3b5adf8d..43d6f70f1add 100644
--- a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java
+++ b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java
@@ -1143,14 +1143,14 @@ public void reportOnContendedMonitorEntered(StaticObject obj) {
         espressoEnv.getEventListener().onContendedMonitorEntered(obj);
     }
 
-    public boolean reportOnMethodEntry(Method.MethodVersion method, Object scope) {
+    public boolean reportOnMethodEntry(Method.MethodVersion methodVersion, Object scope) {
         assert shouldReportVMEvents();
-        return espressoEnv.getEventListener().onMethodEntry(method.getMethod(), scope);
+        return espressoEnv.getEventListener().onMethodEntry(methodVersion.getMethod(), scope);
     }
 
-    public boolean reportOnMethodReturn(Method.MethodVersion method, Object returnValue) {
+    public boolean reportOnMethodReturn(Method.MethodVersion methodVersion, Object returnValue) {
         assert shouldReportVMEvents();
-        return espressoEnv.getEventListener().onMethodReturn(method.getMethod(), returnValue);
+        return espressoEnv.getEventListener().onMethodReturn(methodVersion.getMethod(), returnValue);
     }
 
     public boolean reportOnFieldModification(Field field, StaticObject receiver, Object value) {
diff --git a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/JDWPContextImpl.java b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/JDWPContextImpl.java
index 3ac3e34e5cda..cbeaf8075598 100644
--- a/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/JDWPContextImpl.java
+++ b/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/JDWPContextImpl.java
@@ -59,6 +59,7 @@
 import com.oracle.truffle.espresso.jdwp.api.JDWPContext;
 import com.oracle.truffle.espresso.jdwp.api.KlassRef;
 import com.oracle.truffle.espresso.jdwp.api.MethodRef;
+import com.oracle.truffle.espresso.jdwp.api.MethodVersionRef;
 import com.oracle.truffle.espresso.jdwp.api.ModuleRef;
 import com.oracle.truffle.espresso.jdwp.api.MonitorStackInfo;
 import com.oracle.truffle.espresso.jdwp.api.RedefineInfo;
@@ -306,9 +307,9 @@ public String getStringValue(Object object) {
     }
 
     @Override
-    public MethodRef getMethodFromRootNode(RootNode root) {
+    public MethodVersionRef getMethodFromRootNode(RootNode root) {
         if (root != null && root instanceof EspressoRootNode) {
-            return ((EspressoRootNode) root).getMethod();
+            return ((EspressoRootNode) root).getMethodVersion();
         }
         return null;
     }
@@ -646,7 +647,7 @@ public int getNextBCI(RootNode callerRoot, Frame frame) {
         if (callerRoot instanceof EspressoRootNode espressoRootNode) {
             int bci = (int) readBCIFromFrame(callerRoot, frame);
             if (bci >= 0) {
-                BytecodeStream bs = new BytecodeStream(espressoRootNode.getMethod().getOriginalCode());
+                BytecodeStream bs = new BytecodeStream(espressoRootNode.getMethodVersion().getOriginalCode());
                 return bs.nextBCI(bci);
             }
         }
@@ -665,8 +666,8 @@ public long readBCIFromFrame(RootNode root, Frame frame) {
     public CallFrame locateObjectWaitFrame() {
         Object currentThread = asGuestThread(Thread.currentThread());
         KlassRef klass = context.getMeta().java_lang_Object;
-        MethodRef method = context.getMeta().java_lang_Object_wait;
-        return new CallFrame(ids.getIdAsLong(currentThread), TypeTag.CLASS, ids.getIdAsLong(klass), method, ids.getIdAsLong(method), 0, null, null, null, null, null, LOGGER);
+        MethodVersionRef methodVersion = context.getMeta().java_lang_Object_wait.getMethodVersion();
+        return new CallFrame(ids.getIdAsLong(currentThread), TypeTag.CLASS, ids.getIdAsLong(klass), methodVersion, ids.getIdAsLong(methodVersion.getMethod()), 0, null, null, null, null, null, LOGGER);
     }
 
     @Override