diff a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java
--- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java
+++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java
@@ -35,12 +35,14 @@
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Thread dump support.
  *
  * This class defines methods to dump threads to an output stream or file in plain
@@ -158,15 +160,44 @@
         container.threads().forEach(t -> dumpThread(t, ps));
         container.children().forEach(c -> dumpThreads(c, ps));
     }
 
     private static void dumpThread(Thread thread, PrintStream ps) {
-        String suffix = thread.isVirtual() ? " virtual" : "";
-        ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
-        for (StackTraceElement ste : thread.getStackTrace()) {
+        ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
+        Thread.State state = snapshot.threadState();
+        ps.println("#" + thread.threadId() + " \"" + snapshot.threadName()
+                +  "\" " + state + " " + Instant.now());
+
+        // park blocker
+        Object parkBlocker = snapshot.parkBlocker();
+        if (parkBlocker != null) {
+            ps.println("      // parked on " + Objects.toIdentityString(parkBlocker));
+        }
+
+        // blocked on monitor enter or Object.wait
+        if (state == Thread.State.BLOCKED) {
+            Object obj = snapshot.blockedOn();
+            if (obj != null) {
+                ps.println("      // blocked on " + Objects.toIdentityString(obj));
+            }
+        } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+            Object obj = snapshot.waitingOn();
+            if (obj != null) {
+                ps.println("      // waiting on " + Objects.toIdentityString(obj));
+            }
+        }
+
+        StackTraceElement[] stackTrace = snapshot.stackTrace();
+        int depth = 0;
+        while (depth < stackTrace.length) {
+            snapshot.ownedMonitorsAt(depth).forEach(obj -> {
+                ps.print("      // locked ");
+                ps.println(Objects.toIdentityString(obj));
+            });
             ps.print("      ");
-            ps.println(ste);
+            ps.println(stackTrace[depth]);
+            depth++;
         }
         ps.println();
     }
 
     /**
@@ -185,108 +216,120 @@
     }
 
     /**
      * Generate a thread dump to the given print stream in JSON format.
      */
-    private static void dumpThreadsToJson(PrintStream out) {
-        out.println("{");
-        out.println("  \"threadDump\": {");
-
-        String now = Instant.now().toString();
-        String runtimeVersion = Runtime.version().toString();
-        out.format("    \"processId\": \"%d\",%n", processId());
-        out.format("    \"time\": \"%s\",%n", escape(now));
-        out.format("    \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
-
-        out.println("    \"threadContainers\": [");
-        List<ThreadContainer> containers = allContainers();
-        Iterator<ThreadContainer> iterator = containers.iterator();
-        while (iterator.hasNext()) {
-            ThreadContainer container = iterator.next();
-            boolean more = iterator.hasNext();
-            dumpThreadsToJson(container, out, more);
-        }
-        out.println("    ]");   // end of threadContainers
+    private static void dumpThreadsToJson(PrintStream ps) {
+        try (JsonWriter jsonWriter = JsonWriter.wrap(ps)) {
+            jsonWriter.startObject("threadDump");
+
+            jsonWriter.writeProperty("processId", processId());
+            jsonWriter.writeProperty("time", Instant.now());
+            jsonWriter.writeProperty("runtimeVersion", Runtime.version());
 
-        out.println("  }");   // end threadDump
-        out.println("}");  // end object
+            jsonWriter.startArray("threadContainers");
+            allContainers().forEach(c -> dumpThreadsToJson(c, jsonWriter));
+            jsonWriter.endArray();
+
+            jsonWriter.endObject();  // threadDump
+        }
     }
 
     /**
-     * Dump the given thread container to the print stream in JSON format.
+     * Write a thread container to the given JSON writer.
      */
-    private static void dumpThreadsToJson(ThreadContainer container,
-                                          PrintStream out,
-                                          boolean more) {
-        out.println("      {");
-        out.format("        \"container\": \"%s\",%n", escape(container.toString()));
-
-        ThreadContainer parent = container.parent();
-        if (parent == null) {
-            out.format("        \"parent\": null,%n");
-        } else {
-            out.format("        \"parent\": \"%s\",%n", escape(parent.toString()));
-        }
+    private static void dumpThreadsToJson(ThreadContainer container, JsonWriter jsonWriter) {
+        jsonWriter.startObject();
+        jsonWriter.writeProperty("container", container);
+        jsonWriter.writeProperty("parent", container.parent());
 
         Thread owner = container.owner();
-        if (owner == null) {
-            out.format("        \"owner\": null,%n");
-        } else {
-            out.format("        \"owner\": \"%d\",%n", owner.threadId());
-        }
+        jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
 
         long threadCount = 0;
-        out.println("        \"threads\": [");
+        jsonWriter.startArray("threads");
         Iterator<Thread> threads = container.threads().iterator();
         while (threads.hasNext()) {
             Thread thread = threads.next();
-            dumpThreadToJson(thread, out, threads.hasNext());
+            dumpThreadToJson(thread, jsonWriter);
             threadCount++;
         }
-        out.println("        ],");   // end of threads
+        jsonWriter.endArray(); // threads
 
         // thread count
         if (!ThreadContainers.trackAllThreads()) {
             threadCount = Long.max(threadCount, container.threadCount());
         }
-        out.format("        \"threadCount\": \"%d\"%n", threadCount);
+        jsonWriter.writeProperty("threadCount", threadCount);
 
-        if (more) {
-            out.println("      },");
-        } else {
-            out.println("      }");  // last container, no trailing comma
-        }
+        jsonWriter.endObject();
     }
 
     /**
-     * Dump the given thread and its stack trace to the print stream in JSON format.
+     * Write a thread to the given JSON writer.
      */
-    private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
-        out.println("         {");
-        out.println("           \"tid\": \"" + thread.threadId() + "\",");
-        out.println("           \"name\": \"" + escape(thread.getName()) + "\",");
-        out.println("           \"stack\": [");
-
-        int i = 0;
-        StackTraceElement[] stackTrace = thread.getStackTrace();
-        while (i < stackTrace.length) {
-            out.print("              \"");
-            out.print(escape(stackTrace[i].toString()));
-            out.print("\"");
-            i++;
-            if (i < stackTrace.length) {
-                out.println(",");
-            } else {
-                out.println();  // last element, no trailing comma
+    private static void dumpThreadToJson(Thread thread, JsonWriter jsonWriter) {
+        Instant now = Instant.now();
+        ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
+        Thread.State state = snapshot.threadState();
+        StackTraceElement[] stackTrace = snapshot.stackTrace();
+
+        jsonWriter.startObject();
+        jsonWriter.writeProperty("tid", thread.threadId());
+        jsonWriter.writeProperty("time", now);
+        jsonWriter.writeProperty("name", snapshot.threadName());
+        jsonWriter.writeProperty("state", state);
+
+        // park blocker
+        Object parkBlocker = snapshot.parkBlocker();
+        if (parkBlocker != null) {
+            jsonWriter.startObject("parkBlocker");
+            jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
+            // TBD add exclusiveOwnerThread if AbstractOwnableSynchronizer
+            jsonWriter.endObject();
+        }
+
+        // blocked on monitor enter or Object.wait
+        if (state == Thread.State.BLOCKED) {
+            Object obj = snapshot.blockedOn();
+            if (obj != null) {
+                jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
+            }
+        } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+            Object obj = snapshot.waitingOn();
+            if (obj != null) {
+                jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
             }
         }
-        out.println("           ]");
-        if (more) {
-            out.println("         },");
-        } else {
-            out.println("         }");  // last thread, no trailing comma
+
+        // stack trace
+        jsonWriter.startArray("stack");
+        Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
+        jsonWriter.endArray();
+
+        // monitors owned, skip if none
+        if (snapshot.ownsMonitors()) {
+            jsonWriter.startArray("monitorsOwned");
+            int depth = 0;
+            while (depth < stackTrace.length) {
+                List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
+                if (!objs.isEmpty()) {
+                    jsonWriter.startObject();
+                    jsonWriter.writeProperty("depth", depth);
+                    jsonWriter.startArray("locks");
+                    snapshot.ownedMonitorsAt(depth)
+                            .map(Objects::toIdentityString)
+                            .forEach(jsonWriter::writeProperty);
+                    jsonWriter.endArray();
+                    jsonWriter.endObject();
+                }
+                depth++;
+            }
+            jsonWriter.endArray();
         }
+
+        jsonWriter.endObject();
     }
 
     /**
      * Returns a list of all thread containers that are "reachable" from
      * the root container.
@@ -301,35 +344,172 @@
         containers.add(container);
         container.children().forEach(c -> collect(c, containers));
     }
 
     /**
-     * Escape any characters that need to be escape in the JSON output.
+     * Simple JSON writer to stream objects/arrays to a PrintStream.
      */
-    private static String escape(String value) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < value.length(); i++) {
-            char c = value.charAt(i);
-            switch (c) {
-                case '"'  -> sb.append("\\\"");
-                case '\\' -> sb.append("\\\\");
-                case '/'  -> sb.append("\\/");
-                case '\b' -> sb.append("\\b");
-                case '\f' -> sb.append("\\f");
-                case '\n' -> sb.append("\\n");
-                case '\r' -> sb.append("\\r");
-                case '\t' -> sb.append("\\t");
-                default -> {
-                    if (c <= 0x1f) {
-                        sb.append(String.format("\\u%04x", c));
-                    } else {
-                        sb.append(c);
+    private static class JsonWriter implements AutoCloseable {
+        private final PrintStream out;
+
+        // current depth and indentation
+        private int depth = -1;
+        private int indent;
+
+        // indicates if there are properties at depth N
+        private boolean[] hasProperties = new boolean[10];
+
+        private JsonWriter(PrintStream out) {
+            this.out = out;
+        }
+
+        static JsonWriter wrap(PrintStream out) {
+            var writer = new JsonWriter(out);
+            writer.startObject();
+            return writer;
+        }
+
+        /**
+         * Start of object or array.
+         */
+        private void startObject(String name, boolean array) {
+            if (depth >= 0) {
+                if (hasProperties[depth]) {
+                    out.println(",");
+                } else {
+                    hasProperties[depth] = true;  // first property at this depth
+                }
+            }
+            out.print(" ".repeat(indent));
+            if (name != null) {
+                out.print("\"" + name + "\": ");
+            }
+            if (array) {
+                out.println("[");
+            } else {
+                out.println("{");
+            }
+            indent += 2;
+            depth++;
+            hasProperties[depth] = false;
+        }
+
+        /**
+         * End of object or array.
+         */
+        private void endObject(boolean array) {
+            if (hasProperties[depth]) {
+                out.println();
+                hasProperties[depth] = false;
+            }
+            depth--;
+            indent -= 2;
+            out.print(" ".repeat(indent));
+            if (array) {
+                out.print("]");
+            } else {
+                out.print("}");
+            }
+        }
+
+        /**
+         * Write a property.
+         * @param name the property name, null for an unnamed property
+         * @param obj the value or null
+         */
+        void writeProperty(String name, Object obj) {
+            if (hasProperties[depth]) {
+                out.println(",");
+            } else {
+                hasProperties[depth] = true;
+            }
+            out.print(" ".repeat(indent));
+            if (name != null) {
+                out.print("\"" + name + "\": ");
+            }
+            switch (obj) {
+                case Number _ -> out.print(obj);
+                case null     -> out.print("null");
+                default       -> out.print("\"" + escape(obj.toString()) + "\"");
+            }
+        }
+
+        /**
+         * Write an unnamed property.
+         */
+        void writeProperty(Object obj) {
+            writeProperty(null, obj);
+        }
+
+        /**
+         * Start named object.
+         */
+        void startObject(String name) {
+            startObject(name, false);
+        }
+
+        /**
+         * Start unnamed object.
+         */
+        void startObject() {
+            startObject(null);
+        }
+
+        /**
+         * End of object.
+         */
+        void endObject() {
+            endObject(false);
+        }
+
+        /**
+         * Start named array.
+         */
+        void startArray(String name) {
+            startObject(name, true);
+        }
+
+        /**
+         * End of array.
+         */
+        void endArray() {
+            endObject(true);
+        }
+
+        @Override
+        public void close() {
+            endObject();
+            out.flush();
+        }
+
+        /**
+         * Escape any characters that need to be escape in the JSON output.
+         */
+        private static String escape(String value) {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < value.length(); i++) {
+                char c = value.charAt(i);
+                switch (c) {
+                    case '"'  -> sb.append("\\\"");
+                    case '\\' -> sb.append("\\\\");
+                    case '/'  -> sb.append("\\/");
+                    case '\b' -> sb.append("\\b");
+                    case '\f' -> sb.append("\\f");
+                    case '\n' -> sb.append("\\n");
+                    case '\r' -> sb.append("\\r");
+                    case '\t' -> sb.append("\\t");
+                    default -> {
+                        if (c <= 0x1f) {
+                            sb.append(String.format("\\u%04x", c));
+                        } else {
+                            sb.append(c);
+                        }
                     }
                 }
             }
+            return sb.toString();
         }
-        return sb.toString();
     }
 
     /**
      * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
      * written the subsequent bytes are discarded.
