DDR Support for Virtual Threads in JDK 21

DDR (Direct Dump Reader) is a Java implementation of the DTFJ (Diagnostic Tooling Framework for Java) API. It works by walking the J9 structures inside a dump to extract the VM and application state. It’s useful for inspecting Java objects at a certain point of execution of a program. For example, you can list all the threads and inspect the stack trace of a thread.

In Java 19, the Virtual Thread was introduced by Project Loom. A virtual thread is a lightweight implementation of java.lang.Thread which allows people to write highly concurrent applications. In contrast to a platform thread, a virtual thread is not tied to any OS thread but is mounted on a platform thread. A virtual thread can be unmounted if a blocking operation is encountered, and then its continuation is no longer associated with any OS thread.

With more and more applications using virtual threads, supporting virtual threads in DDR will become essential for debugging programs with virtual threads. To support virtual threads, three new DDR commands were added: vthreads, continuationstack and continuationstackslots.

An Example

We will introduce the new command with the following example:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = Thread.ofVirtual().name("vthread 1").start(() -> {
        System.out.println("virtual thread 1 starts");
        LockSupport.park();
        System.out.println("virtual thread 1 ends");
    });

    while (t1.getState() != Thread.State.WAITING) {
        Thread.sleep(500); // let vthread 1 park
    }

    Thread t2 = Thread.ofVirtual().name("vthread 2").start(() -> {
        System.out.println("virtual thread 2 starts");
        com.ibm.jvm.Dump.systemDumpToFile();
        LockSupport.unpark(t1);
        System.out.println("virtual thread 2 ends");
    });

    t1.join();
    t2.join();
}

In the example, two virtual threads are created and one of them is parked, thus unmounted.

In order to get the desired core file, you may need to run the example with -Xdump:system:request=exclusive+prepwalk option.

The vthreads Command

The vthreads command lists all the virtual threads in the core file.

Let’s open the generated core file with jdmpview, and call the vthreads command by typing !vthreads and we get the output:

> !vthreads
!continuationstack 0x00007f1d94003410 !j9vmcontinuation 0x00007f1d94003410 !j9object 0x00000007FFF9A848 (Continuation) !j9object 0x00000007FFF9A7A0 (VThread) - vthread 2
!continuationstack 0x00007f1d94010110 !j9vmcontinuation 0x00007f1d94010110 !j9object 0x00000007FFF8C598 (Continuation) !j9object 0x00000007FFF8C490 (VThread) - vthread 1

In each line, four commands are listed to:

  • inspect the stack trace of the continuation
  • inspect the native continuation struct
  • inspect the continuation object
  • inspect the virtual thread object

respectively. The name of the virtual thread will also be shown at the end of the line (if the name is set).

The vthreads command is usually the first step that you want to do after opening the core file. You can identify the virtual thread that you want to work on either by looking at the names or by checking the stack trace which we will cover in the following section.

The continuationstack and continuationstackslots Commands

The stack trace is useful for quickly obtaining diagnostic information after a crash or at a certain point of program execution. The continuationstack command outputs the stack trace of a continuation. Running !continuationstack 0x00007f1d94003410 (the continuation of virtual thread 2) will output:

> !continuationstack 0x00007f1d94003410
<7f1d94003410>  !j9method 0x00000000000A58F0   jdk/internal/vm/Continuation.enterImpl()Z
<7f1d94003410>  !j9method 0x00000000000A5770   jdk/internal/vm/Continuation.run()V
<7f1d94003410>  !j9method 0x0000000000096118   java/lang/VirtualThread.runContinuation()V
<7f1d94003410>  !j9method 0x000000000026D388   java/lang/VirtualThread$$Lambda/0x0000000000000000.run()V
<7f1d94003410>  !j9method 0x000000000026F388   java/util/concurrent/ForkJoinTask$RunnableExecuteAction.exec()Z
<7f1d94003410>  !j9method 0x000000000026E108   java/util/concurrent/ForkJoinTask.doExec()I
<7f1d94003410>  !j9method 0x000000000025E440   java/util/concurrent/ForkJoinPool$WorkQueue.topLevelExec(Ljava/util/concurrent/ForkJoinTask;Ljava/util/concurrent/ForkJoinPool$WorkQueue;)V
<7f1d94003410>  !j9method 0x000000000025BC10   java/util/concurrent/ForkJoinPool.scan(Ljava/util/concurrent/ForkJoinPool$WorkQueue;II)I
<7f1d94003410>  !j9method 0x000000000025BBF0   java/util/concurrent/ForkJoinPool.runWorker(Ljava/util/concurrent/ForkJoinPool$WorkQueue;)V
<7f1d94003410>  !j9method 0x00000000001FFF30   java/util/concurrent/ForkJoinWorkerThread.run()V
<7f1d94003410>                          JNI call-in frame
<7f1d94003410>                          Native method frame

Note that this is actually the stack trace of the carrier thread. This is because virtual thread 2 is mounted and during the mounting process, some of the continuation’s variables, such as the stack pointer, are swapped with the carrier thread’s. So the continuationstack command will output the stack trace of the carrier thread for a mounted virtual thread. To view the stack trace of the virtual thread, use the stack command with the carrier thread:

> !stack 0x0023d700
<23d700>        !j9method 0x0000000000286A20   com/ibm/jvm/Dump.triggerDumpsImpl(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
<23d700>        !j9method 0x00000000002868C0   com/ibm/jvm/Dump.triggerDump(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
<23d700>        !j9method 0x00000000002867C0   com/ibm/jvm/Dump.systemDumpToFile()Ljava/lang/String;
<23d700>        !j9method 0x0000000000246800   VTTest2.lambda$main$1(Ljava/lang/Thread;)V
<23d700>        !j9method 0x0000000000246DE8   VTTest2$$Lambda/0x000000001436ed78.run()V
<23d700>        !j9method 0x00000000000961D8   java/lang/VirtualThread.runWith(Ljava/lang/Object;Ljava/lang/Runnable;)V
<23d700>        !j9method 0x00000000000961B8   java/lang/VirtualThread.run(Ljava/lang/Runnable;)V
<23d700>        !j9method 0x000000000026C770   java/lang/VirtualThread$VThreadContinuation$1.run()V
<23d700>        !j9method 0x00000000000A5750   jdk/internal/vm/Continuation.enter(Ljdk/internal/vm/Continuation;)V
<23d700>                                JNI call-in frame
<23d700>                                Native method frame

For an unmounted virtual thread, the continuationstack command will output the stack trace of the virtual thread:

> !continuationstack 0x00007f1d94010110
<7f1d94010110>  !j9method 0x00000000000A5910   jdk/internal/vm/Continuation.yieldImpl(Z)Z
<7f1d94010110>  !j9method 0x00000000000A57B0   jdk/internal/vm/Continuation.yield0()Z
<7f1d94010110>  !j9method 0x00000000000A5790   jdk/internal/vm/Continuation.yield(Ljdk/internal/vm/ContinuationScope;)Z
<7f1d94010110>  !j9method 0x0000000000096298   java/lang/VirtualThread.yieldContinuation()Z
<7f1d94010110>  !j9method 0x0000000000096378   java/lang/VirtualThread.park()V
<7f1d94010110>  !j9method 0x00000000000B9198   java/lang/Access.parkVirtualThread()V
<7f1d94010110>  !j9method 0x0000000000278110   jdk/internal/misc/VirtualThreads.park()V
<7f1d94010110>  !j9method 0x000000000008F2C8   java/util/concurrent/locks/LockSupport.park()V
<7f1d94010110>  !j9method 0x0000000000246820   VTTest2.lambda$main$0()V
<7f1d94010110>  !j9method 0x0000000000246AE8   VTTest2$$Lambda/0x000000001436ec28.run()V
<7f1d94010110>  !j9method 0x00000000000961D8   java/lang/VirtualThread.runWith(Ljava/lang/Object;Ljava/lang/Runnable;)V
<7f1d94010110>  !j9method 0x00000000000961B8   java/lang/VirtualThread.run(Ljava/lang/Runnable;)V
<7f1d94010110>  !j9method 0x000000000026C770   java/lang/VirtualThread$VThreadContinuation$1.run()V
<7f1d94010110>  !j9method 0x00000000000A5750   jdk/internal/vm/Continuation.enter(Ljdk/internal/vm/Continuation;)V
<7f1d94010110>                          JNI call-in frame
<7f1d94010110>                          Native method frame

Additionally, similar to the stack and stackslots commands, we also have the continuationstackslots command which outputs slots along with the stack trace.

Leave a Reply