On Wed, 28 Oct 2026 06:01:51 GMT, Shiv Shah <[email protected]> wrote:

>> When a virtual thread suspended at a breakpoint gets unmounted and mounted 
>> again only the top frame is thawed. The caller stays frozen in the stack 
>> chunk and the popped frame returns into the continuation return barrier. The 
>> interpreter's popframe entry treats that return address like a deoptimized 
>> caller, preserves the arguments, sets the reexecution bit and returns. The 
>> barrier then thawed the caller as a plain return. Arguments and bit left 
>> behind, no reexecution of the invoke, run() went on with a zero result.
>> 
>> Thaw finishes it now. An interpreted caller gets the preserved arguments 
>> back on its expression stack and its return slot is patched to the 
>> interpreter deopt entry after push_return_frame, the x86 deopt entry reloads 
>> rsp from last_sp for that, aarch64 already does. A compiled caller frozen in 
>> the chunk is deoptimized from UpdateForPopTopFrameClosure::doit the same way 
>> set_frame_pop does it. Thaw leaves the arguments alone in that case and 
>> unpack_on_stack puts them back. A frozen compiled frame being popped stays 
>> opaque.
>> 
>> A linkTo caller needs one more thing. The linker consumed the trailing 
>> MemberName before the target ran and the preserved arguments are sized by 
>> the target, so the MemberName is not among them. Thaw takes it from the 
>> caller's local 0 the way the interpreter's popframe entry does, the lookup 
>> from member_name_arg_or_null moved into a plain helper both can call. The 
>> arguments go one slot up and the MemberName goes back on top.
>> 
>> popframes002 runs popframes001 on a virtual thread with the join Chris 
>> Plummer wrote enabled by a property in the debuggee. It unmounts the tested 
>> thread before the breakpoint so the caller is frozen when popped, and run() 
>> is excluded from compilation so this stays the interpreted case. 
>> popframes003 is the compiled caller case, -Xcomp with the breakpoint in a 
>> separate stop() so testedMethod keeps its compiled frame and run() stays 
>> frozen, the debugger pops frame 1 and the debuggee checks with WhiteBox that 
>> run() and testedMethod are compiled and that testedMethod ran twice. 
>> -XX:+PreserveFramePointer keeps the thaw on the slow path. Both fail on 
>> master without the hotspot change. popframes001 itself runs the plain case 
>> as before and is off the virtual problem list. If separate copies of the 
>> debugger and debuggee are preferred over the properties that is easy to 
>> change. PopFrameMethodNameInvariantTest got an unmount mode, the same 
>> linkToStatic pop on a virtual thread that sleeps
 before the breakp...
>
> Shiv Shah has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   Add the invokeExact cases, the LambdaForm frame keeps the 
> DirectMethodHandle in local0

Overall changes look good to me. I think reusing the deopt entry may be 
problematic on some platforms, and it would be better to add a dedicated entry 
for pop frame. Some other comments and suggestions below, thanks.

src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp line 235:

> 233:     __ jcc(Assembler::zero, L);
> 234:     __ lea(rsp, Address(rbp, rscratch1, Address::times_ptr));
> 235:     __ bind(L);

I’m not a fan of adjusting the deopt entry just to be able to reuse it for the 
pop frame case. Also, looking at ppc for example, it seems the deopt entry 
assumes the interpreter state has already been restored by the deoptimization 
code. I think it would be better to have a dedicated pop frame entry, even if 
the implementation on some platform just jumps to the deopt one.

src/hotspot/share/interpreter/interpreterRuntime.cpp line 1566:

> 1564: #endif
> 1565: 
> 1566: #if INCLUDE_JVMTI

The `#if INCLUDE_JVMTI` conditional should stay here since 
`popframe_member_name_arg_or_null` is only used by JVMTI. Same in header file.

src/hotspot/share/prims/jvmtiEnvBase.cpp line 2556:

> 2554:       assert(_state->is_virtual(), "invariant");
> 2555:       if (frame_count == 0) {
> 2556:         _result = JVMTI_ERROR_OPAQUE_FRAME;

Suggestion:

        // If the top frame is in the heap, PopFrame is not supported.
        _result = JVMTI_ERROR_OPAQUE_FRAME;

And I would remove the comment above.

src/hotspot/share/prims/jvmtiEnvBase.cpp line 2560:

> 2558:       }
> 2559:       javaVFrame* jvf = vfs.asJavaVFrame();
> 2560:       if (!jvf->fr().can_be_deoptimized()) {

This check should also apply for the stack frame case (although I don’t see how 
it can fail for either case at this point).

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3088:

> 3086: // unpack_on_stack puts the arguments back. An interpreted caller was 
> copied
> 3087: // without them, so put them back on its expression stack and resume at 
> the
> 3088: // invoke, like vframeArrayElement::unpack_on_stack does.

Suggestion:

// A frame popped by JVMTI PopFrame returned into the continuation return
// barrier. The interpreter's popframe entry preserved the arguments and
// set the deopt reexecution bit as for a deoptimized caller. If the caller
// is compiled, UpdateForPopTopFrameClosure already marked it for 
deoptimization,
// so we just return and let the deoptimization code handle the pop frame
// as usual. If the caller is interpreted, we need to copy the arguments
// back in the expression stack and patch the return address so that the
// invoke is reexecuted.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3101:

> 3099: 
> 3100: // Called after fix_thawed_frame, the locals must be in their thawed 
> form to
> 3101: // read the MemberName.

Suggestion:

// Called after fix_thawed_frame in case we need to recover MemberName from 
local0.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3104:

> 3102: static void popframe_restore_args(JavaThread* thread, const frame& f) {
> 3103:   // the thawed frame's sp already covers the argument slots, last_sp 
> was
> 3104:   // saved after the arguments were pushed

Suggestion:

  // The expression stack already includes the callee arguments, since last_sp
  // was saved after they were pushed.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3106:

> 3104:   // saved after the arguments were pushed
> 3105:   int top_element = f.interpreter_frame_expression_stack_size() - 1;
> 3106:   // for a linkTo* call the trailing MemberName was consumed by the 
> linker and

Suggestion:

  // For a linkTo* call the trailing MemberName was consumed by the linker and

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3107:

> 3105:   int top_element = f.interpreter_frame_expression_stack_size() - 1;
> 3106:   // for a linkTo* call the trailing MemberName was consumed by the 
> linker and
> 3107:   // is not among the preserved arguments, take it from local 0 like the

Suggestion:

  // is not among the preserved arguments. Take it from local 0 like the

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3108:

> 3106:   // for a linkTo* call the trailing MemberName was consumed by the 
> linker and
> 3107:   // is not among the preserved arguments, take it from local 0 like the
> 3108:   // interpreter's popframe entry does

Suggestion:

  // interpreter's popframe entry does.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3111:

> 3109:   Method* method = f.interpreter_frame_method();
> 3110:   address local0 = method->max_locals() > 0 ? 
> (address)*f.interpreter_frame_local_at(0) : nullptr;
> 3111:   oop member_name = 
> InterpreterRuntime::popframe_member_name_arg_or_null(local0, method, 
> f.interpreter_frame_bcp());

Suggestion:

  oop member_name = 
InterpreterRuntime::popframe_member_name_arg_or_null(local0, method, 
f.interpreter_frame_bcp());

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3114:

> 3112:   int words = in_words(thread->popframe_preserved_args_size_in_words());
> 3113:   if (words > 0) {
> 3114:     int first = top_element - (member_name != nullptr ? 1 : 0);

Suggestion:

    assert(f.interpreter_frame_expression_stack_size() >= words + (member_name 
!= nullptr ? 1 : 0), "no room for arguments");
    int first = top_element - (member_name != nullptr ? 1 : 0);

An alternative stronger assert is:

#ifdef ASSERT
    InterpreterOopMap mask;
    f.interpreted_frame_oop_map(&mask);
    assert(f.interpreter_frame_expression_stack_size() ==
               mask.expression_stack_size() + words + (member_name != nullptr ? 
1 : 0), "not enough room for arguments");
#endif

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3121:

> 3119:     *f.interpreter_frame_expression_stack_at(top_element) = 
> cast_from_oop<intptr_t>(member_name);
> 3120:   }
> 3121:   thread->clear_popframe_condition();

We should also restore the mdp before reexecuting the call, as we do in the 
interpreter for the normal pop frame case.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3123:

> 3121:   thread->clear_popframe_condition();
> 3122: }
> 3123: 

These methods could be moved into the existing `#if INCLUDE_JVMTI` block above, 
alongside the other JVMTI methods.

src/hotspot/share/runtime/continuationFreezeThaw.cpp line 3158:

> 3156:   if (popframe_reexecute) {
> 3157:     popframe_restore_args(_thread, f);
> 3158:   }

We can consolidate all these 3 steps into a single method call here, wrapped in 
`JVMTI_ONLY()`.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 62:

> 60: 
> 61: /*
> 62:  * @test id=unmount

Suggestion:

 * @test id=virtual

This would make it consistent with `id=platform` above. Using `unmount` would 
make sense if there are different runs with and without unmounting 
(`id=virtual`, `id=virtual-unmount`). Same with the `unmount-mh` variant.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 65:

> 63:  * @bug 8391440
> 64:  * @summary MemberName in local0 must be preserved when PopFrame 
> re-executes direct MethodHandle.linkToStatic
> 65:  *          also when that caller is frozen in a virtual thread's stack 
> chunk

Suggestion:

 *          in a caller restored from the heap

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 73:

> 71:  *          jdk.jdi
> 72:  * @build   java.base/java.lang.invoke.MethodHandleHelper
> 73:  * @run driver runtime.interpreter.PopFrameMethodNameInvariantTest unmount

Suggestion:

 * @run driver runtime.interpreter.PopFrameMethodNameInvariantTest virtual

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 77:

> 75: 
> 76: /*
> 77:  * @test id=platform-mh

Suggestion:

 * @test id=platform-invokeExact

Both variants use method handle machinery so this makes it more clear. Same in 
the vthread variant.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 87:

> 85:  *          jdk.jdi
> 86:  * @build   java.base/java.lang.invoke.MethodHandleHelper
> 87:  * @run driver runtime.interpreter.PopFrameMethodNameInvariantTest mh

Suggestion:

 * @run driver runtime.interpreter.PopFrameMethodNameInvariantTest invokeExact

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 94:

> 92:  * @bug 8391440
> 93:  * @summary MemberName must be restored from the DirectMethodHandle in 
> local0 when PopFrame re-executes
> 94:  *          linkToStatic in a LambdaForm frame frozen in a virtual 
> thread's stack chunk

Suggestion:

 *          linkToStatic in a LambdaForm caller restored from the heap

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 107:

> 105: public class PopFrameMethodNameInvariantTest {
> 106:     public static class Target {
> 107:         static final boolean UNMOUNT = 
> Boolean.getBoolean("popframe.unmount");

Suggestion:

        static final boolean VIRTUAL = Boolean.getBoolean("popframe.virtual");

And related code below.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 111:

> 109:         static final long LONG_ARG = 0x1234567890ABL;
> 110:         static final Object MARKER = new Object();
> 111:         static final boolean USE_MH = Boolean.getBoolean("popframe.mh");

Suggestion:

        static final boolean USE_INVOKE_EXACT = 
Boolean.getBoolean("popframe.invokeExact");

And related code below.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 120:

> 118:             if (UNMOUNT) {
> 119:                 // sleeping unmounts this virtual thread, when it comes 
> back the
> 120:                 // frames below body stay frozen until they are returned 
> into

Suggestion:

                // Sleep unmounts the virtual thread. After mounting again, the
                // caller of body is not thawed, so PopFrame must restore it
                // from the heap.

test/hotspot/jtreg/runtime/interpreter/PopFrameMethodNameInvariantTest.java 
line 131:

> 129: 
> 130:         // invokeExact reaches body through a LambdaForm frame that 
> keeps the DirectMethodHandle in local0,
> 131:         // the helper keeps the MemberName there

Suggestion:

        // The caller of body keeps a DirectMethodHandle in local 0 for 
invokeExact,
        // or a MemberName for the explicit linkToStatic call.

test/hotspot/jtreg/vmTestbase/nsk/jdwp/StackFrame/PopFrames/popframes001a.java 
line 68:

> 66:         log.display("  ... thread created");
> 67: 
> 68:         if (TestedThreadClass.UNMOUNT) {

We don’t need the extra `UNMOUNT` parameter as it is a common path for both 
virtual thread tests. So this check could be `thread.isVirtual()` instead. Same 
below in `testedMethod` (or check `parker`).

test/hotspot/jtreg/vmTestbase/nsk/jdwp/StackFrame/PopFrames/popframes001a.java 
line 70:

> 68:         if (TestedThreadClass.UNMOUNT) {
> 69:             // the thread the tested method joins, it ends once the 
> tested thread is parked in that join.
> 70:             // It is made here so testedMethod has nothing before the 
> join that could deoptimize it

Suggestion:

            // The thread joined by the virtual thread in testedMethod. It 
exits once the
            // virtual thread has unmounted. Created here to avoid setup code 
in testedMethod
            // that could trigger deoptimization before join().

test/hotspot/jtreg/vmTestbase/nsk/jdwp/StackFrame/PopFrames/popframes001a.java 
line 96:

> 94: 
> 95:         // popping only stop() would reach the breakpoint twice as well
> 96:         if (TestedThreadClass.COMPILED_CALLER && 
> TestedThreadClass.invokations != 2) {

Shouldn't this be true for all cases and not just for the compiled caller case? 
Also preexisting typo, it should be `invocations`.

test/hotspot/jtreg/vmTestbase/nsk/jdwp/StackFrame/PopFrames/popframes001a.java 
line 149:

> 147:                 // the first join unmounts a virtual tested thread, so 
> the frame below is frozen when popped,
> 148:                 // the parker is gone by the second one. Nothing before 
> this join may deoptimize this frame
> 149:                 // or the thaw brings the caller out with it

Suggestion:

                // The first join unmounts the tested thread, leaving its 
caller in the
                // heap when PopFrame is executed. Code before this join must 
not deoptimize
                // this frame, as thawing an interpreted frame also thaws its 
compiled caller.

-------------

PR Review: https://git.openjdk.org/jdk/pull/32661#pullrequestreview-5717597602
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427822572
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427645070
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427692220
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427698735
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427726797
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427735218
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427744284
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427748654
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427752152
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427749782
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427756060
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427774760
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427792298
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427797106
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427801480
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427850875
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427866477
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427869254
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427890540
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427902209
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427908853
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427911422
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427919590
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427934048
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427943021
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427962571
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427971310
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427979030
PR Review Comment: https://git.openjdk.org/jdk/pull/32661#discussion_r4427986796

Reply via email to