From 52bb3de739e798df6de4cbe6e0031b6b2fd777b4 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Sat, 24 May 2025 07:42:40 +0100 Subject: [PATCH 1/4] Initial commit --- .../classes/jdk/internal/vm/ThreadDumper.java | 586 ++++++++++----- .../management/HotSpotDiagnosticMXBean.java | 10 +- .../doc-files/threadDump.schema.json | 175 +++++ .../internal/HotSpotDiagnostic.java | 4 +- .../dcmd/thread/ThreadDumpToFileTest.java | 27 +- .../HotSpotDiagnosticMXBean/DumpThreads.java | 706 ++++++++++++++---- .../DumpThreadsWithEliminatedLock.java | 171 +++++ .../jdk/test/lib/threaddump/ThreadDump.java | 286 ++++--- 8 files changed, 1546 insertions(+), 419 deletions(-) create mode 100644 src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json create mode 100644 test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreadsWithEliminatedLock.java diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index d0705bc245761..1b5b08331bcf6 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 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 @@ -24,11 +24,13 @@ */ package jdk.internal.vm; -import java.io.BufferedOutputStream; +import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; @@ -36,15 +38,20 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Instant; -import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.AbstractOwnableSynchronizer; /** * Thread dump support. * - * This class defines methods to dump threads to an output stream or file in plain - * text or JSON format. + * This class defines static methods to support the Thread.dump_to_file diagnostic command + * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a + * thread dump to a file or byte array in plain text or JSON format. */ public class ThreadDumper { private ThreadDumper() { } @@ -53,13 +60,12 @@ private ThreadDumper() { } private static final int MAX_BYTE_ARRAY_SIZE = 16_000; /** - * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded. - * + * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded. * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. * * @param file the file path to the file, null or "-" to return a byte array * @param okayToOverwrite true to overwrite an existing file - * @return the UTF-8 encoded thread dump or message to return to the user + * @return the UTF-8 encoded thread dump or message to return to the tool user */ public static byte[] dumpThreads(String file, boolean okayToOverwrite) { if (file == null || file.equals("-")) { @@ -70,13 +76,12 @@ public static byte[] dumpThreads(String file, boolean okayToOverwrite) { } /** - * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded. - * + * Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded. * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. * * @param file the file path to the file, null or "-" to return a byte array * @param okayToOverwrite true to overwrite an existing file - * @return the UTF-8 encoded thread dump or message to return to the user + * @return the UTF-8 encoded thread dump or message to return to the tool user */ public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) { if (file == null || file.equals("-")) { @@ -88,21 +93,32 @@ public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) { /** * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded. + * This method is the implementation of the Thread.dump_to_file diagnostic command + * when a file path is not specified. It returns the thread and/or message to send + * to the tool user. */ private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { - try (var out = new BoundedByteArrayOutputStream(maxSize); - PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) { + var out = new BoundedByteArrayOutputStream(maxSize); + try (out; var writer = new TextWriter(out)) { if (json) { - dumpThreadsToJson(ps); + dumpThreadsToJson(writer); } else { - dumpThreads(ps); + dumpThreads(writer); } - return out.toByteArray(); + } catch (Exception ex) { + if (ex instanceof UncheckedIOException ioe) { + ex = ioe.getCause(); + } + String reply = String.format("Failed: %s%n", ex); + return reply.getBytes(StandardCharsets.UTF_8); } + return out.toByteArray(); } /** * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded. + * This method is the implementation of the Thread.dump_to_file diagnostic command. + * It returns the thread and/or message to send to the tool user. */ private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) { Path path = Path.of(file).toAbsolutePath(); @@ -110,224 +126,408 @@ private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, bo ? new OpenOption[0] : new OpenOption[] { StandardOpenOption.CREATE_NEW }; String reply; - try (OutputStream out = Files.newOutputStream(path, options); - BufferedOutputStream bos = new BufferedOutputStream(out); - PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) { - if (json) { - dumpThreadsToJson(ps); - } else { - dumpThreads(ps); + try (OutputStream out = Files.newOutputStream(path, options)) { + try (var writer = new TextWriter(out)) { + if (json) { + dumpThreadsToJson(writer); + } else { + dumpThreads(writer); + } + reply = String.format("Created %s%n", path); + } catch (UncheckedIOException e) { + reply = String.format("Failed: %s%n", e.getCause()); } - reply = String.format("Created %s%n", path); - } catch (FileAlreadyExistsException e) { + } catch (FileAlreadyExistsException _) { reply = String.format("%s exists, use -overwrite to overwrite%n", path); - } catch (IOException ioe) { - reply = String.format("Failed: %s%n", ioe); + } catch (Exception ex) { + reply = String.format("Failed: %s%n", ex); } return reply.getBytes(StandardCharsets.UTF_8); } /** - * Generate a thread dump in plain text format to the given output stream, - * UTF-8 encoded. - * - * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. + * Generate a thread dump in plain text format to the given output stream, UTF-8 + * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. + * @throws IOException if an I/O error occurs */ - public static void dumpThreads(OutputStream out) { - BufferedOutputStream bos = new BufferedOutputStream(out); - PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); + public static void dumpThreads(OutputStream out) throws IOException { + var writer = new TextWriter(out); try { - dumpThreads(ps); - } finally { - ps.flush(); // flushes underlying stream + dumpThreads(writer); + writer.flush(); + } catch (UncheckedIOException e) { + IOException ioe = e.getCause(); + throw ioe; } } /** - * Generate a thread dump in plain text format to the given print stream. + * Generate a thread dump in plain text format to the given text stream. + * @throws UncheckedIOException if an I/O error occurs */ - private static void dumpThreads(PrintStream ps) { - ps.println(processId()); - ps.println(Instant.now()); - ps.println(Runtime.version()); - ps.println(); - dumpThreads(ThreadContainers.root(), ps); + private static void dumpThreads(TextWriter writer) { + writer.println(processId()); + writer.println(Instant.now()); + writer.println(Runtime.version()); + writer.println(); + dumpThreads(ThreadContainers.root(), writer); } - private static void dumpThreads(ThreadContainer container, PrintStream ps) { - container.threads().forEach(t -> dumpThread(t, ps)); - container.children().forEach(c -> dumpThreads(c, ps)); + private static void dumpThreads(ThreadContainer container, TextWriter writer) { + container.threads().forEach(t -> dumpThread(t, writer)); + container.children().forEach(c -> dumpThreads(c, writer)); } - 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()) { - ps.print(" "); - ps.println(ste); + private static void dumpThread(Thread thread, TextWriter writer) { + ThreadSnapshot snapshot = ThreadSnapshot.of(thread); + Instant now = Instant.now(); + Thread.State state = snapshot.threadState(); + writer.println("#" + thread.threadId() + " \"" + snapshot.threadName() + + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now); + + // park blocker + Object parkBlocker = snapshot.parkBlocker(); + if (parkBlocker != null) { + writer.print(" // parked on " + Objects.toIdentityString(parkBlocker)); + if (parkBlocker instanceof AbstractOwnableSynchronizer + && snapshot.exclusiveOwnerThread() instanceof Thread owner) { + writer.print(", owned by #" + owner.threadId()); + } + writer.println(); + } + + // blocked on monitor enter or Object.wait + if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) { + writer.println(" // blocked on " + Objects.toIdentityString(obj)); + } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) + && snapshot.waitingOn() instanceof Object obj) { + writer.println(" // waiting on " + Objects.toIdentityString(obj)); + } + + StackTraceElement[] stackTrace = snapshot.stackTrace(); + int depth = 0; + while (depth < stackTrace.length) { + snapshot.ownedMonitorsAt(depth).forEach(o -> { + if (o != null) { + writer.println(" // locked " + Objects.toIdentityString(o)); + } else { + writer.println(" // lock is eliminated"); + } + }); + writer.print(" "); + writer.println(stackTrace[depth]); + depth++; } - ps.println(); + writer.println(); } /** * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded. - * * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. + * @throws IOException if an I/O error occurs */ - public static void dumpThreadsToJson(OutputStream out) { - BufferedOutputStream bos = new BufferedOutputStream(out); - PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); + public static void dumpThreadsToJson(OutputStream out) throws IOException { + var writer = new TextWriter(out); try { - dumpThreadsToJson(ps); - } finally { - ps.flush(); // flushes underlying stream + dumpThreadsToJson(writer); + writer.flush(); + } catch (UncheckedIOException e) { + IOException ioe = e.getCause(); + throw ioe; } } /** - * Generate a thread dump to the given print stream in JSON format. + * Generate a thread dump to the given text stream in JSON format. + * @throws UncheckedIOException if an I/O error occurs */ - 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 containers = allContainers(); - Iterator iterator = containers.iterator(); - while (iterator.hasNext()) { - ThreadContainer container = iterator.next(); - boolean more = iterator.hasNext(); - dumpThreadsToJson(container, out, more); - } - out.println(" ]"); // end of threadContainers - - out.println(" }"); // end threadDump - out.println("}"); // end object + private static void dumpThreadsToJson(TextWriter textWriter) { + var jsonWriter = new JsonWriter(textWriter); + + jsonWriter.startObject(); // top-level object + + jsonWriter.startObject("threadDump"); + + jsonWriter.writeProperty("processId", processId()); + jsonWriter.writeProperty("time", Instant.now()); + jsonWriter.writeProperty("runtimeVersion", Runtime.version()); + + jsonWriter.startArray("threadContainers"); + dumpThreads(ThreadContainers.root(), jsonWriter); + jsonWriter.endArray(); + + jsonWriter.endObject(); // threadDump + + jsonWriter.endObject(); // end of top-level object } /** - * Dump the given thread container to the print stream in JSON format. + * Write a thread container to the given JSON writer. + * @throws UncheckedIOException if an I/O error occurs */ - 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 dumpThreads(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 threads = container.threads().iterator(); while (threads.hasNext()) { Thread thread = threads.next(); - dumpThreadToJson(thread, out, threads.hasNext()); + dumpThread(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(); + + // the children of the thread container follow + container.children().forEach(c -> dumpThreads(c, jsonWriter)); } /** - * Dump the given thread and its stack trace to the print stream in JSON format. + * Write a thread to the given JSON writer. + * @throws UncheckedIOException if an I/O error occurs */ - 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 dumpThread(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); + if (thread.isVirtual()) { + jsonWriter.writeProperty("virtual", Boolean.TRUE); + } + 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)); + if (parkBlocker instanceof AbstractOwnableSynchronizer + && snapshot.exclusiveOwnerThread() instanceof Thread owner) { + jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId()); } + jsonWriter.endObject(); } - out.println(" ]"); - if (more) { - out.println(" },"); - } else { - out.println(" }"); // last thread, no trailing comma + + // blocked on monitor enter or Object.wait + if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) { + jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj)); + } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) + && snapshot.waitingOn() instanceof Object obj) { + jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj)); } - } - /** - * Returns a list of all thread containers that are "reachable" from - * the root container. - */ - private static List allContainers() { - List containers = new ArrayList<>(); - collect(ThreadContainers.root(), containers); - return containers; - } + // stack trace + jsonWriter.startArray("stack"); + Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty); + jsonWriter.endArray(); - private static void collect(ThreadContainer container, List containers) { - containers.add(container); - container.children().forEach(c -> collect(c, containers)); + // monitors owned, skip if none + if (snapshot.ownsMonitors()) { + jsonWriter.startArray("monitorsOwned"); + int depth = 0; + while (depth < stackTrace.length) { + List objs = snapshot.ownedMonitorsAt(depth).toList(); + if (!objs.isEmpty()) { + jsonWriter.startObject(); + jsonWriter.writeProperty("depth", depth); + jsonWriter.startArray("locks"); + snapshot.ownedMonitorsAt(depth) + .map(o -> (o != null) ? Objects.toIdentityString(o) : null) + .forEach(jsonWriter::writeProperty); + jsonWriter.endArray(); + jsonWriter.endObject(); + } + depth++; + } + jsonWriter.endArray(); + } + + // thread identifier of carrier, when mounted + if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) { + jsonWriter.writeProperty("carrier", carrier.threadId()); + } + + jsonWriter.endObject(); } /** - * Escape any characters that need to be escape in the JSON output. + * Simple JSON writer to stream objects/arrays to a TextWriter with formatting. + * This class is not intended to be a fully featured JSON writer. */ - 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 { + private static class Node { + final boolean isArray; + int propertyCount; + Node(boolean isArray) { + this.isArray = isArray; + } + boolean isArray() { + return isArray; + } + int propertyCount() { + return propertyCount; + } + int getAndIncrementPropertyCount() { + int old = propertyCount; + propertyCount++; + return old; + } + } + private final Deque stack = new ArrayDeque<>(); + private final TextWriter writer; + + JsonWriter(TextWriter writer) { + this.writer = writer; + } + + private void indent() { + int indent = stack.size() * 2; + writer.print(" ".repeat(indent)); + } + + /** + * Start of object or array. + */ + private void startObject(String name, boolean isArray) { + if (!stack.isEmpty()) { + Node node = stack.peek(); + if (node.getAndIncrementPropertyCount() > 0) { + writer.println(","); + } + } + indent(); + if (name != null) { + writer.print("\"" + name + "\": "); + } + writer.println(isArray ? "[" : "{"); + stack.push(new Node(isArray)); + } + + /** + * End of object or array. + */ + private void endObject(boolean isArray) { + Node node = stack.pop(); + if (node.isArray() != isArray) + throw new IllegalStateException(); + if (node.propertyCount() > 0) { + writer.println(); + } + indent(); + writer.print(isArray ? "]" : "}"); + } + + /** + * 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) { + Node node = stack.peek(); + if (node.getAndIncrementPropertyCount() > 0) { + writer.println(","); + } + indent(); + if (name != null) { + writer.print("\"" + name + "\": "); + } + switch (obj) { + // Long may be larger than safe range of JSON integer value + case Long _ -> writer.print("\"" + obj + "\""); + case Number _ -> writer.print(obj); + case Boolean _ -> writer.print(obj); + case null -> writer.print("null"); + default -> writer.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); + } + + /** + * 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(); } /** @@ -357,6 +557,56 @@ public void close() { } } + /** + * Simple Writer implementation for printing text. The print/println methods + * throw UncheckedIOException if an I/O error occurs. + */ + private static class TextWriter extends Writer { + private final Writer delegate; + + TextWriter(OutputStream out) { + delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + delegate.write(cbuf, off, len); + } + + void print(Object obj) { + String s = String.valueOf(obj); + try { + write(s, 0, s.length()); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + void println() { + print(System.lineSeparator()); + } + + void println(String s) { + print(s); + println(); + } + + void println(Object obj) { + print(obj); + println(); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + } + /** * Returns the process ID or -1 if not supported. */ diff --git a/src/jdk.management/share/classes/com/sun/management/HotSpotDiagnosticMXBean.java b/src/jdk.management/share/classes/com/sun/management/HotSpotDiagnosticMXBean.java index fba648461555b..067cfffb158b8 100644 --- a/src/jdk.management/share/classes/com/sun/management/HotSpotDiagnosticMXBean.java +++ b/src/jdk.management/share/classes/com/sun/management/HotSpotDiagnosticMXBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 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 @@ -116,6 +116,13 @@ public interface HotSpotDiagnosticMXBean extends PlatformManagedObject { * {@code outputFile} parameter must be an absolute path to a file that * does not exist. * + *

When the format is specified as {@link ThreadDumpFormat#JSON JSON}, the + * thread dump is generated in JavaScript Object Notation. + * threadDump.schema.json + * describes the thread dump format in draft + * + * JSON Schema Language version 2. + * *

The thread dump will include output for all platform threads. It may * include output for some or all virtual threads. * @@ -151,6 +158,7 @@ public static enum ThreadDumpFormat { TEXT_PLAIN, /** * JSON (JavaScript Object Notation) format. + * @spec https://datatracker.ietf.org/doc/html/rfc8259 JavaScript Object Notation */ JSON, } diff --git a/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json new file mode 100644 index 0000000000000..711b69f1b0fb0 --- /dev/null +++ b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json @@ -0,0 +1,175 @@ +{ + "type": "object", + "properties": { + "threadDump": { + "type": "object", + "properties": { + "processId": { + "type": "string", + "description": "The native process id of the Java virtual machine." + }, + "time": { + "type": "string", + "description": "The time in ISO 8601 format when the thread dump was generated." + }, + "runtimeVersion": { + "type": "string", + "description": "The runtime version, see java.lang.Runtime.Version" + }, + "threadContainers": { + "type": "array", + "description": "The array of thread containers (thread groupings).", + "items": [ + { + "type": "object", + "properties": { + "container": { + "type": "string", + "description": "The container name. The container name is unique." + }, + "parent": { + "type": [ + "string", + "null" + ], + "description": "The parent container name or null for the root container." + }, + "owner": { + "type": [ + "string", + "null" + ], + "description": "The thread identifier of the owner thread if owned." + }, + "threads": { + "type": "array", + "description": "The array of threads in the thread container.", + "items": [ + { + "type": "object", + "properties": { + "tid": { + "type": "string", + "description": "The thread identifier." + }, + "time": { + "type": "string", + "description": "The time in ISO 8601 format that the thread was sampled." + }, + "name": { + "type": "string", + "description": "The thread name." + }, + "state": { + "type": "string", + "description": "The thread state (Thread::getState)." + }, + "virtual" : { + "type": "boolean", + "description": "true for a virtual thread." + }, + "parkBlocker": { + "type": [ + "object" + ], + "properties": { + "object": { + "type": "string", + "description": "The blocker object responsible for the thread parking." + }, + "exclusiveOwnerThreadId": { + "type": "string", + "description": "The thread identifier of the owner when the blocker object has an owner." + } + }, + "required": [ + "object" + ] + }, + "blockedOn": { + "type": "string", + "description": "The object that the thread is blocked on waiting to enter/re-enter a synchronization block/method." + }, + "waitingOn": { + "type": "string", + "description": "The object that the thread is waiting to be notified (Object.wait)." + }, + "stack": { + "type": "array", + "description": "The thread stack. The first element is the top of the stack.", + "items": [ + { + "type": "string", + "description": "A stack trace element (java.lang.StackTraceElement)." + } + ] + }, + "monitorsOwned": { + "type": "array", + "description": "The objects for which monitors are owned by the thread.", + "items": { + "type": "object", + "properties": { + "depth": { + "type": "integer", + "description": "The stack depth at which the monitors are owned." + }, + "locks": { + "type": "array", + "items": { + "type": [ + "string", + null + ], + "description": "The object for which the monitor is owned by the thread, null if eliminated" + } + } + }, + "required": [ + "depth", + "locks" + ] + } + }, + "carrier": { + "type": "string", + "description": "The thread identifier of the carrier thread if mounted." + } + }, + "required": [ + "tid", + "time", + "name", + "state", + "stack" + ] + } + ] + }, + "threadCount": { + "type": "string", + "description": "The number of threads in the thread container." + } + }, + "required": [ + "container", + "parent", + "owner", + "threads" + ] + } + ] + } + }, + "required": [ + "processId", + "time", + "runtimeVersion", + "threadContainers" + ] + } + }, + "required": [ + "threadDump" + ] +} diff --git a/src/jdk.management/share/classes/com/sun/management/internal/HotSpotDiagnostic.java b/src/jdk.management/share/classes/com/sun/management/internal/HotSpotDiagnostic.java index 855e800a794c3..4e5f20fa1414c 100644 --- a/src/jdk.management/share/classes/com/sun/management/internal/HotSpotDiagnostic.java +++ b/src/jdk.management/share/classes/com/sun/management/internal/HotSpotDiagnostic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 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 @@ -153,7 +153,7 @@ public void dumpThreads(String outputFile, ThreadDumpFormat format) throws IOExc throw new IllegalArgumentException("'outputFile' not absolute path"); try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW)) { - dumpThreads(out, format); + dumpThreads(out, format); } } diff --git a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java index f6002a3296852..1fcc609114a0c 100644 --- a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java +++ b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 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 @@ -25,6 +25,7 @@ * @test * @bug 8284161 8287008 * @summary Basic test for jcmd Thread.dump_to_file + * @modules jdk.jcmd * @library /test/lib * @run junit/othervm ThreadDumpToFileTest */ @@ -66,7 +67,8 @@ void testPlainThreadDump() throws IOException { @Test void testJsonThreadDump() throws IOException { Path file = genThreadDumpPath(".json"); - jcmdThreadDumpToFile(file, "-format=json").shouldMatch("Created"); + jcmdThreadDumpToFile(file, "-format=json") + .shouldMatch("Created"); // parse the JSON text String jsonText = Files.readString(file); @@ -89,7 +91,8 @@ void testDoNotOverwriteFile() throws IOException { Path file = genThreadDumpPath(".txt"); Files.writeString(file, "xxx"); - jcmdThreadDumpToFile(file, "").shouldMatch("exists"); + jcmdThreadDumpToFile(file, "") + .shouldMatch("exists"); // file should not be overridden assertEquals("xxx", Files.readString(file)); @@ -102,7 +105,23 @@ void testDoNotOverwriteFile() throws IOException { void testOverwriteFile() throws IOException { Path file = genThreadDumpPath(".txt"); Files.writeString(file, "xxx"); - jcmdThreadDumpToFile(file, "-overwrite"); + jcmdThreadDumpToFile(file, "-overwrite") + .shouldMatch("Created"); + } + + /** + * Test output file cannot be created. + */ + @Test + void testFileCreateFails() throws IOException { + Path badFile = Path.of(".").toAbsolutePath() + .resolve("does-not-exist") + .resolve("does-not-exist") + .resolve("threads.bad"); + jcmdThreadDumpToFile(badFile, "-format=plain") + .shouldMatch("Failed"); + jcmdThreadDumpToFile(badFile, "-format=json") + .shouldMatch("Failed"); } /** diff --git a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java index a4072f2f7ff64..5a3a4c3bd0d6f 100644 --- a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java +++ b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 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 @@ -21,41 +21,64 @@ * questions. */ -/** +/* * @test - * @bug 8284161 8287008 8309406 + * @bug 8284161 8287008 8309406 8356870 * @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads * @requires vm.continuations - * @modules jdk.management + * @modules java.base/jdk.internal.vm jdk.management * @library /test/lib - * @run junit/othervm DumpThreads - * @run junit/othervm -Djdk.trackAllThreads DumpThreads - * @run junit/othervm -Djdk.trackAllThreads=true DumpThreads - * @run junit/othervm -Djdk.trackAllThreads=false DumpThreads + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * --enable-native-access=ALL-UNNAMED DumpThreads + * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads DumpThreads + * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=true DumpThreads + * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=false DumpThreads */ import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Path; -import java.util.Objects; import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.regex.Pattern; +import java.util.regex.Matcher; import com.sun.management.HotSpotDiagnosticMXBean; import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat; import jdk.test.lib.threaddump.ThreadDump; +import jdk.test.lib.thread.VThreadPinner; +import jdk.test.lib.thread.VThreadRunner; +import jdk.test.whitebox.WhiteBox; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; class DumpThreads { private static boolean trackAllThreads; @@ -64,6 +87,56 @@ class DumpThreads { static void setup() throws Exception { String s = System.getProperty("jdk.trackAllThreads"); trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s); + + // need >=2 carriers for testing pinning + VThreadRunner.ensureParallelism(2); + } + + /** + * Test thread dump in plain text format. + */ + @Test + void testPlainText() throws Exception { + List lines = dumpThreadsToPlainText(); + + // pid should be on the first line + String pid = Long.toString(ProcessHandle.current().pid()); + assertEquals(pid, lines.get(0)); + + // timestamp should be on the second line + String secondLine = lines.get(1); + ZonedDateTime.parse(secondLine); + + // runtime version should be on third line + String vs = Runtime.version().toString(); + assertEquals(vs, lines.get(2)); + + // dump should include current thread + Thread currentThread = Thread.currentThread(); + if (trackAllThreads || !currentThread.isVirtual()) { + ThreadFields fields = findThread(currentThread.threadId(), lines); + assertNotNull(fields, "current thread not found"); + assertEquals(currentThread.getName(), fields.name()); + assertEquals(currentThread.isVirtual(), fields.isVirtual()); + } + } + + /** + * Test thread dump in JSON format. + */ + @Test + void testJsonFormat() throws Exception { + ThreadDump threadDump = dumpThreadsToJson(); + + // dump should include current thread in the root container + Thread currentThread = Thread.currentThread(); + if (trackAllThreads || !currentThread.isVirtual()) { + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(currentThread.threadId()) + .orElse(null); + assertNotNull(ti, "current thread not found"); + assertEquals(currentThread.isVirtual(), ti.isVirtual()); + } } /** @@ -78,180 +151,432 @@ static Stream executors() { } /** - * Test thread dump in plain text format contains information about the current - * thread and a virtual thread created directly with the Thread API. + * Test that a thread container for an executor service is in the JSON format thread dump. */ - @Test - void testRootContainerPlainTextFormat() throws Exception { - Thread vthread = Thread.ofVirtual().start(LockSupport::park); - try { - testDumpThreadsPlainText(vthread, trackAllThreads); - } finally { - LockSupport.unpark(vthread); + @ParameterizedTest + @MethodSource("executors") + void testThreadContainer(ExecutorService executor) throws Exception { + try (executor) { + testThreadContainer(executor, Objects.toIdentityString(executor)); } } /** - * Test thread dump in JSON format contains information about the current - * thread and a virtual thread created directly with the Thread API. + * Test that a thread container for the common pool is in the JSON format thread dump. */ @Test - void testRootContainerJsonFormat() throws Exception { - Thread vthread = Thread.ofVirtual().start(LockSupport::park); + void testCommonPool() throws Exception { + testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool"); + } + + /** + * Test that the JSON thread dump has a thread container for the given executor. + */ + private void testThreadContainer(ExecutorService executor, String name) throws Exception { + var threadRef = new AtomicReference(); + + executor.submit(() -> { + threadRef.set(Thread.currentThread()); + LockSupport.park(); + }); + + // capture Thread + Thread thread; + while ((thread = threadRef.get()) == null) { + Thread.sleep(20); + } + try { - testDumpThreadsJson(null, vthread, trackAllThreads); + // dump threads to file and parse as JSON object + ThreadDump threadDump = dumpThreadsToJson(); + + // find the thread container corresponding to the executor + var container = threadDump.findThreadContainer(name).orElse(null); + assertNotNull(container, name + " not found"); + assertFalse(container.owner().isPresent()); + var parent = container.parent().orElse(null); + assertEquals(threadDump.rootThreadContainer(), parent); + + // find the thread in the thread container + ThreadDump.ThreadInfo ti = container.findThread(thread.threadId()).orElse(null); + assertNotNull(ti, "thread not found"); + } finally { - LockSupport.unpark(vthread); + LockSupport.unpark(thread); } } /** - * Test thread dump in plain text format includes a thread executing a task in the - * given ExecutorService. + * ThreadFactory implementations for tests. */ - @ParameterizedTest - @MethodSource("executors") - void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception { - try (executor) { - Thread thread = forkParker(executor); - try { - testDumpThreadsPlainText(thread, true); - } finally { - LockSupport.unpark(thread); - } + static Stream threadFactories() { + Stream s = Stream.of(Thread.ofPlatform().factory()); + if (trackAllThreads) { + return Stream.concat(s, Stream.of(Thread.ofVirtual().factory())); + } else { + return s; } } /** - * Test thread dump in JSON format includes a thread executing a task in the - * given ExecutorService. + * Test thread dump with a thread blocked on monitor enter. */ @ParameterizedTest - @MethodSource("executors") - void testExecutorServiceJsonFormat(ExecutorService executor) throws Exception { - try (executor) { - Thread thread = forkParker(executor); - try { - testDumpThreadsJson(Objects.toIdentityString(executor), thread, true); - } finally { - LockSupport.unpark(thread); - } - } + @MethodSource("threadFactories") + void testBlockedThread(ThreadFactory factory) throws Exception { + testBlockedThread(factory, false); } /** - * Test thread dump in JSON format includes a thread executing a task in the - * fork-join common pool. + * Test thread dump with a thread blocked on monitor enter when pinned. */ @Test - void testForkJoinPool() throws Exception { - ForkJoinPool pool = ForkJoinPool.commonPool(); - Thread thread = forkParker(pool); + void testBlockedThreadWhenPinned() throws Exception { + assumeTrue(trackAllThreads, "This test requires all threads to be tracked"); + testBlockedThread(Thread.ofVirtual().factory(), true); + } + + void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception { + var lock = new Object(); + var started = new CountDownLatch(1); + + Thread thread = factory.newThread(() -> { + if (pinned) { + VThreadPinner.runPinned(() -> { + started.countDown(); + synchronized (lock) { } // blocks + }); + } else { + started.countDown(); + synchronized (lock) { } // blocks + } + }); + try { - testDumpThreadsJson("ForkJoinPool.commonPool", thread, true); + synchronized (lock) { + // start thread and wait for it to block + thread.start(); + started.await(); + await(thread, Thread.State.BLOCKED); + + long tid = thread.threadId(); + String lockAsString = Objects.toIdentityString(lock); + + // thread dump in plain text should include thread + List lines = dumpThreadsToPlainText(); + ThreadFields fields = findThread(tid, lines); + assertNotNull(fields, "thread not found"); + assertEquals("BLOCKED", fields.state()); + assertTrue(contains(lines, "// blocked on " + lockAsString)); + + // thread dump in JSON format should include thread in root container + ThreadDump threadDump = dumpThreadsToJson(); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(tid) + .orElse(null); + assertNotNull(ti, "thread not found"); + assertEquals("BLOCKED", ti.state()); + assertEquals(lockAsString, ti.blockedOn()); + if (pinned) { + assertTrue(ti.carrier().isPresent(), "carrier not found"); + } + } } finally { - LockSupport.unpark(thread); + thread.join(); } } /** - * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in plain text - * format, then sanity check that the thread dump includes expected strings, the - * current thread, and maybe the given thread. - * @param thread the thread to test if included - * @param expectInDump true if the thread is expected to be included + * Test thread dump with a thread waiting in Object.wait. */ - private void testDumpThreadsPlainText(Thread thread, boolean expectInDump) throws Exception { - Path file = genOutputPath(".txt"); - var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); - mbean.dumpThreads(file.toString(), ThreadDumpFormat.TEXT_PLAIN); - System.err.format("Dumped to %s%n", file); + @ParameterizedTest + @MethodSource("threadFactories") + void testWaitingThread(ThreadFactory factory) throws Exception { + testWaitingThread(factory, false); + } - // pid should be on the first line - String line1 = line(file, 0); - String pid = Long.toString(ProcessHandle.current().pid()); - assertTrue(line1.contains(pid)); + /** + * Test thread dump with a thread waiting in Object.wait when pinned. + */ + @Test + void testWaitingThreadWhenPinned() throws Exception { + assumeTrue(trackAllThreads, "This test requires all threads to be tracked"); + testWaitingThread(Thread.ofVirtual().factory(), true); + } - // timestamp should be on the second line - String line2 = line(file, 1); - ZonedDateTime.parse(line2); + void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception { + var lock = new Object(); + var started = new CountDownLatch(1); - // runtime version should be on third line - String line3 = line(file, 2); - String vs = Runtime.version().toString(); - assertTrue(line3.contains(vs)); + Thread thread = factory.newThread(() -> { + try { + synchronized (lock) { + if (pinned) { + VThreadPinner.runPinned(() -> { + started.countDown(); + lock.wait(); + }); + } else { + started.countDown(); + lock.wait(); + } + } + } catch (InterruptedException e) { } + }); - // test if thread is included in thread dump - assertEquals(expectInDump, isPresent(file, thread)); + try { + // start thread and wait for it to wait in Object.wait + thread.start(); + started.await(); + await(thread, Thread.State.WAITING); + + long tid = thread.threadId(); + String lockAsString = Objects.toIdentityString(lock); + + // thread dump in plain text should include thread + List lines = dumpThreadsToPlainText(); + ThreadFields fields = findThread(tid, lines); + assertNotNull(fields, "thread not found"); + assertEquals("WAITING", fields.state()); + + // thread dump in JSON format should include thread in root container + ThreadDump threadDump = dumpThreadsToJson(); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(thread.threadId()) + .orElse(null); + assertNotNull(ti, "thread not found"); + assertEquals(ti.isVirtual(), thread.isVirtual()); + assertEquals("WAITING", ti.state()); + if (pinned) { + assertTrue(ti.carrier().isPresent(), "carrier not found"); + } - // current thread should be included if platform thread or tracking all threads - Thread currentThread = Thread.currentThread(); - boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual(); - assertEquals(currentThreadExpected, isPresent(file, currentThread)); + // Compiled native frames have no locals. If Object.wait0 has been compiled + // then we don't have the object that the thread is waiting on + Method wait0 = Object.class.getDeclaredMethod("wait0", long.class); + boolean expectWaitingOn = !WhiteBox.getWhiteBox().isMethodCompiled(wait0); + if (expectWaitingOn) { + // plain text dump should have "waiting on" line + assertTrue(contains(lines, "// waiting on " + lockAsString)); + + // JSON thread dump should have waitingOn property + assertEquals(lockAsString, ti.waitingOn()); + } + + } finally { + synchronized (lock) { + lock.notifyAll(); + } + thread.join(); + } } /** - * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in JSON format. - * The thread dump is parsed as a JSON object and checked to ensure that it contains - * expected data, the current thread, and maybe the given thread. - * @param containerName the name of the container or null for the root container - * @param thread the thread to test if included - * @param expect true if the thread is expected to be included + * Test thread dump with a thread parked on a j.u.c. lock. */ - private void testDumpThreadsJson(String containerName, - Thread thread, - boolean expectInDump) throws Exception { - Path file = genOutputPath(".json"); - var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); - mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON); - System.err.format("Dumped to %s%n", file); + @ParameterizedTest + @MethodSource("threadFactories") + void testParkedThread(ThreadFactory factory) throws Exception { + testParkedThread(factory, false); + } - // parse the JSON text - String jsonText = Files.readString(file); - ThreadDump threadDump = ThreadDump.parse(jsonText); + /** + * Test thread dump with a thread parked on a j.u.c. lock and pinned. + */ + @Test + void testParkedThreadWhenPinned() throws Exception { + assumeTrue(trackAllThreads, "This test requires all threads to be tracked"); + testParkedThread(Thread.ofVirtual().factory(), true); + } - // test threadDump/processId - assertTrue(threadDump.processId() == ProcessHandle.current().pid()); + void testParkedThread(ThreadFactory factory, boolean pinned) throws Exception { + var lock = new ReentrantLock(); + var started = new CountDownLatch(1); + + Thread thread = factory.newThread(() -> { + if (pinned) { + VThreadPinner.runPinned(() -> { + started.countDown(); + lock.lock(); + lock.unlock(); + }); + } else { + started.countDown(); + lock.lock(); + lock.unlock(); + } + }); - // test threadDump/time can be parsed - ZonedDateTime.parse(threadDump.time()); + lock.lock(); + try { + // start thread and wait for it to park + thread.start(); + started.await(); + await(thread, Thread.State.WAITING); + + long tid = thread.threadId(); + + // thread dump in plain text should include thread + List lines = dumpThreadsToPlainText(); + ThreadFields fields = findThread(tid, lines); + assertNotNull(fields, "thread not found"); + assertEquals("WAITING", fields.state()); + assertTrue(contains(lines, "// parked on java.util.concurrent.locks.ReentrantLock")); + + // thread dump in JSON format should include thread in root container + ThreadDump threadDump = dumpThreadsToJson(); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(thread.threadId()) + .orElse(null); + assertNotNull(ti, "thread not found"); + assertEquals(ti.isVirtual(), thread.isVirtual()); + + // thread should be waiting on the ReentrantLock, owned by the main thread. + assertEquals("WAITING", ti.state()); + String parkBlocker = ti.parkBlocker(); + assertNotNull(parkBlocker); + assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock")); + long ownerTid = ti.exclusiveOwnerThreadId().orElse(-1L); + assertEquals(Thread.currentThread().threadId(), ownerTid); + if (pinned) { + assertTrue(ti.carrier().isPresent(), "carrier not found"); + } + } finally { + lock.unlock(); + thread.join(); + } + } - // test threadDump/runtimeVersion - assertEquals(Runtime.version().toString(), threadDump.runtimeVersion()); + /** + * Test thread dump with a thread owning a monitor. + */ + @ParameterizedTest + @MethodSource("threadFactories") + void testThreadOwnsMonitor(ThreadFactory factory) throws Exception { + testThreadOwnsMonitor(factory, false); + } - // test root container, has no parent and no owner - var rootContainer = threadDump.rootThreadContainer(); - assertFalse(rootContainer.owner().isPresent()); - assertFalse(rootContainer.parent().isPresent()); + @Test + void testThreadOwnsMonitorWhenPinned() throws Exception { + assumeTrue(trackAllThreads, "This test requires all threads to be tracked"); + testThreadOwnsMonitor(Thread.ofVirtual().factory(), true); + } - // test that the container contains the given thread - ThreadDump.ThreadContainer container; - if (containerName == null) { - // root container, the thread should be found if trackAllThreads is true - container = rootContainer; - } else { - // find the container - container = threadDump.findThreadContainer(containerName).orElse(null); - assertNotNull(container, containerName + " not found"); - assertFalse(container.owner().isPresent()); - assertTrue(container.parent().get() == rootContainer); + void testThreadOwnsMonitor(ThreadFactory factory, boolean pinned) throws Exception { + var lock = new Object(); + var started = new CountDownLatch(1); + + Thread thread = factory.newThread(() -> { + synchronized (lock) { + if (pinned) { + VThreadPinner.runPinned(() -> { + started.countDown(); + LockSupport.park(); + }); + } else { + started.countDown(); + LockSupport.park(); + } + } + }); + try { + // start thread and wait for it to park + thread.start(); + started.await(); + await(thread, Thread.State.WAITING); + + long tid = thread.threadId(); + String lockAsString = Objects.toIdentityString(lock); + + // thread dump in plain text should include thread + List lines = dumpThreadsToPlainText(); + ThreadFields fields = findThread(tid, lines); + assertNotNull(fields, "thread not found"); + assertEquals("WAITING", fields.state()); + assertTrue(contains(lines, "// locked " + lockAsString)); + + // thread dump in JSON format should include thread in root container + ThreadDump threadDump = dumpThreadsToJson(); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(tid) + .orElse(null); + assertNotNull(ti, "thread not found"); + assertEquals(ti.isVirtual(), thread.isVirtual()); + + // the lock should be in the ownedMonitors array + Set ownedMonitors = ti.ownedMonitors().values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toSet()); + assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found"); + } finally { + LockSupport.unpark(thread); + thread.join(); } - boolean found = container.findThread(thread.threadId()).isPresent(); - assertEquals(expectInDump, found); + } - // current thread should be in root container if platform thread or tracking all threads - Thread currentThread = Thread.currentThread(); - boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual(); - found = rootContainer.findThread(currentThread.threadId()).isPresent(); - assertEquals(currentThreadExpected, found); + /** + * Test mounted virtual thread. + */ + @Test + void testMountedVirtualThread() throws Exception { + assumeTrue(trackAllThreads, "This test requires all threads to be tracked"); + + // start virtual thread that spins until done + var started = new AtomicBoolean(); + var done = new AtomicBoolean(); + var thread = Thread.ofVirtual().start(() -> { + started.set(true); + while (!done.get()) { + Thread.onSpinWait(); + } + }); + + try { + // wait for thread to start + awaitTrue(started); + long tid = thread.threadId(); + + // thread dump in plain text should include thread + List lines = dumpThreadsToPlainText(); + ThreadFields fields = findThread(tid, lines); + assertNotNull(fields, "thread not found"); + assertTrue(fields.isVirtual()); + + // thread dump in JSON format should include thread in root container + ThreadDump threadDump = dumpThreadsToJson(); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(tid) + .orElse(null); + assertNotNull(ti, "thread not found"); + assertTrue(ti.isVirtual()); + assertTrue(ti.carrier().isPresent(), "carrier not found"); + } finally { + done.set(true); + thread.join(); + } + } + + /** + * Asserts that the given thread identifier is a ForkJoinWorkerThread. + */ + void assertCarrier(long tid) { + Thread thread = Thread.getAllStackTraces() + .keySet() + .stream() + .filter(t -> t.threadId() == tid) + .findAny() + .orElse(null); + assertNotNull(thread, "thread " + tid + " not found"); + assertTrue(thread instanceof ForkJoinWorkerThread, "not a ForkJoinWorkerThread"); } /** * Test that dumpThreads throws if the output file already exists. */ @Test - void testFileAlreadyExsists() throws Exception { + void testFileAlreadyExists() throws Exception { var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); String file = Files.createFile(genOutputPath("txt")).toString(); assertThrows(FileAlreadyExistsException.class, @@ -260,11 +585,44 @@ void testFileAlreadyExsists() throws Exception { () -> mbean.dumpThreads(file, ThreadDumpFormat.JSON)); } + /** + * Test that dumpThreads throws IOException when the output file cannot be created. + */ + @Test + void testFileCreateFails() { + var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + String badFile = Path.of(".").toAbsolutePath() + .resolve("does-not-exist") + .resolve("does-not-exist") + .resolve("threads.bad") + .toString(); + assertThrows(IOException.class, + () -> mbean.dumpThreads(badFile, ThreadDumpFormat.TEXT_PLAIN)); + assertThrows(IOException.class, + () -> mbean.dumpThreads(badFile, ThreadDumpFormat.JSON)); + } + + /** + * Test that dumpThreads throws IOException if writing to output file fails. + */ + @Test + void testFileWriteFails() { + var out = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("There is not enough space on the disk"); + } + }; + // need to invoke internal API directly to test this + assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreads(out)); + assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreadsToJson(out)); + } + /** * Test that dumpThreads throws if the file path is relative. */ @Test - void testRelativePath() throws Exception { + void testRelativePath() { var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); assertThrows(IllegalArgumentException.class, () -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN)); @@ -276,7 +634,7 @@ void testRelativePath() throws Exception { * Test that dumpThreads throws with null parameters. */ @Test - void testNull() throws Exception { + void testNull() { var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); assertThrows(NullPointerException.class, () -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN)); @@ -285,31 +643,63 @@ void testNull() throws Exception { } /** - * Submits a parking task to the given executor, returns the Thread object of - * the parked thread. + * Represents the data for a thread found in a plain text thread dump. */ - private static Thread forkParker(ExecutorService executor) { - class Box { static volatile Thread thread;} - var latch = new CountDownLatch(1); - executor.submit(() -> { - Box.thread = Thread.currentThread(); - latch.countDown(); - LockSupport.park(); - }); - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + private record ThreadFields(long tid, String name, boolean isVirtual, String state) { } + + /** + * Find a thread in the lines of a plain text thread dump. + */ + private ThreadFields findThread(long tid, List lines) { + String line = lines.stream() + .filter(l -> l.startsWith("#" + tid + " ")) + .findFirst() + .orElse(null); + if (line == null) { + return null; } - return Box.thread; + + // #3 "main" RUNNABLE 2025-04-18T15:22:12.012450Z + // #36 "" virtual WAITING 2025-04-18T15:22:12.012450Z + Pattern pattern = Pattern.compile("#(\\d+)\\s+\"([^\"]*)\"\\s+(virtual\\s+)?(\\w+)\\s+(.*)"); + Matcher matcher = pattern.matcher(line); + assertTrue(matcher.matches()); + String name = matcher.group(2); + boolean isVirtual = "virtual ".equals(matcher.group(3)); + String state = matcher.group(4); + return new ThreadFields(tid, name, isVirtual, state); } /** - * Returns true if a Thread is present in a plain text thread dump. + * Returns true if lines of a plain text thread dump contain the given text. */ - private static boolean isPresent(Path file, Thread thread) throws Exception { - String expect = "#" + thread.threadId(); - return count(file, expect) > 0; + private boolean contains(List lines, String text) { + return lines.stream().map(String::trim) + .anyMatch(l -> l.contains(text)); + } + + /** + * Dump threads to a file in plain text format, return the lines in the file. + */ + private List dumpThreadsToPlainText() throws Exception { + Path file = genOutputPath(".txt"); + var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN); + System.err.format("Dumped to %s%n", file.getFileName()); + List lines = Files.readAllLines(file); + return lines; + } + + /** + * Dump threads to a file in JSON format, parse and return as JSON object. + */ + private static ThreadDump dumpThreadsToJson() throws Exception { + Path file = genOutputPath(".json"); + var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON); + System.err.format("Dumped to %s%n", file.getFileName()); + String jsonText = Files.readString(file); + return ThreadDump.parse(jsonText); } /** @@ -323,21 +713,23 @@ private static Path genOutputPath(String suffix) throws Exception { } /** - * Return the count of the number of files in the given file that contain - * the given character sequence. + * Waits for the given thread to get to a given state. */ - static long count(Path file, CharSequence cs) throws Exception { - try (Stream stream = Files.lines(file)) { - return stream.filter(line -> line.contains(cs)).count(); + private void await(Thread thread, Thread.State expectedState) throws InterruptedException { + Thread.State state = thread.getState(); + while (state != expectedState) { + assertTrue(state != Thread.State.TERMINATED, "Thread has terminated"); + Thread.sleep(10); + state = thread.getState(); } } /** - * Return line $n of the given file. + * Waits for the boolean value to become true. */ - private String line(Path file, long n) throws Exception { - try (Stream stream = Files.lines(file)) { - return stream.skip(n).findFirst().orElseThrow(); + private static void awaitTrue(AtomicBoolean ref) throws Exception { + while (!ref.get()) { + Thread.sleep(20); } } } diff --git a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreadsWithEliminatedLock.java b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreadsWithEliminatedLock.java new file mode 100644 index 0000000000000..b94b2f706a84e --- /dev/null +++ b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreadsWithEliminatedLock.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 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. + */ + +/* + * @test + * @bug 8356870 + * @summary Test HotSpotDiagnosticMXBean.dumpThreads with a thread owning a monitor for + * an object that is scalar replaced + * @requires !vm.debug & (vm.compMode != "Xcomp") + * @requires (vm.opt.TieredStopAtLevel == null | vm.opt.TieredStopAtLevel == 4) + * @modules jdk.management + * @library /test/lib + * @run main/othervm DumpThreadsWithEliminatedLock plain platform + * @run main/othervm DumpThreadsWithEliminatedLock plain virtual + * @run main/othervm DumpThreadsWithEliminatedLock json platform + * @run main/othervm DumpThreadsWithEliminatedLock json virtual + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import com.sun.management.HotSpotDiagnosticMXBean; +import jdk.test.lib.threaddump.ThreadDump; +import jdk.test.lib.thread.VThreadRunner; + +public class DumpThreadsWithEliminatedLock { + + public static void main(String[] args) throws Exception { + boolean plain = switch (args[0]) { + case "plain" -> true; + case "json" -> false; + default -> throw new RuntimeException("Unknown dump format"); + }; + + ThreadFactory factory = switch (args[1]) { + case "platform" -> Thread.ofPlatform().factory(); + case "virtual" -> Thread.ofVirtual().factory(); + default -> throw new RuntimeException("Unknown thread kind"); + }; + + // need at least two carriers for JTREG_TEST_THREAD_FACTORY=Virtual + if (Thread.currentThread().isVirtual()) { + VThreadRunner.ensureParallelism(2); + } + + // A thread that spins creating and adding to a StringBuffer. StringBuffer is + // synchronized, assume object will be scalar replaced and the lock eliminated. + var done = new AtomicBoolean(); + var ref = new AtomicReference(); + Thread thread = factory.newThread(() -> { + while (!done.get()) { + StringBuffer sb = new StringBuffer(); + sb.append(System.currentTimeMillis()); + String s = sb.toString(); + ref.set(s); + } + }); + try { + thread.start(); + if (plain) { + testPlainFormat(); + } else { + testJsonFormat(thread.threadId()); + } + } finally { + done.set(true); + thread.join(); + } + } + + /** + * Invoke HotSpotDiagnosticMXBean.dumpThreads to generate a thread dump in plain text + * format until "lock is eliminated" is found in the output. + */ + private static void testPlainFormat() { + try { + Path file = genOutputPath(".txt"); + boolean found = false; + int attempts = 0; + while (!found) { + attempts++; + Files.deleteIfExists(file); + ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class) + .dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN); + try (Stream stream = Files.lines(file)) { + found = stream.map(String::trim) + .anyMatch(l -> l.contains("// lock is eliminated")); + } + System.out.format("%s Attempt %d, found: %b%n", Instant.now(), attempts, found); + } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + /** + * Invoke HotSpotDiagnosticMXBean.dumpThreads to generate a thread dump in JSON format + * until the monitorsOwned.locks array for the given thread has a null lock. + */ + private static void testJsonFormat(long tid) { + try { + Path file = genOutputPath(".json"); + boolean found = false; + int attempts = 0; + while (!found) { + attempts++; + Files.deleteIfExists(file); + ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class) + .dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON); + + // parse thread dump as JSON and find thread + String jsonText = Files.readString(file); + ThreadDump threadDump = ThreadDump.parse(jsonText); + ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer() + .findThread(tid) + .orElse(null); + if (ti == null) { + throw new RuntimeException("Thread " + tid + " not found in thread dump"); + } + + // look for null element in ownedMonitors/locks array + found = ti.ownedMonitors() + .values() + .stream() + .flatMap(List::stream) + .anyMatch(o -> o == null); + System.out.format("%s Attempt %d, found: %b%n", Instant.now(), attempts, found); + } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + /** + * Generate a file path with the given suffix to use as an output file. + */ + private static Path genOutputPath(String suffix) throws IOException { + Path dir = Path.of(".").toAbsolutePath(); + Path file = Files.createTempFile(dir, "dump", suffix); + Files.delete(file); + return file; + } +} \ No newline at end of file diff --git a/test/lib/jdk/test/lib/threaddump/ThreadDump.java b/test/lib/jdk/test/lib/threaddump/ThreadDump.java index 5e4f6ebc10f59..d12419e687d95 100644 --- a/test/lib/jdk/test/lib/threaddump/ThreadDump.java +++ b/test/lib/jdk/test/lib/threaddump/ThreadDump.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 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 @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -62,6 +63,7 @@ * { * "tid": "8", * "name": "Reference Handler", + * "state": "RUNNABLE", * "stack": [ * "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)", * "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:245)", @@ -113,23 +115,46 @@ * } */ public final class ThreadDump { - private final long processId; - private final String time; - private final String runtimeVersion; - private ThreadContainer rootThreadContainer; + private final ThreadContainer rootThreadContainer; + private final Map nameToThreadContainer; + private final JSONValue threadDumpObj; + + private ThreadDump(ThreadContainer rootThreadContainer, + Map nameToThreadContainer, + JSONValue threadDumpObj) { + this.rootThreadContainer = rootThreadContainer; + this.nameToThreadContainer = nameToThreadContainer; + this.threadDumpObj = threadDumpObj; + } /** * Represents an element in the threadDump/threadContainers array. */ public static class ThreadContainer { private final String name; - private long owner; - private ThreadContainer parent; - private Set threads; + private final ThreadContainer parent; private final Set children = new HashSet<>(); + private final JSONValue containerObj; - ThreadContainer(String name) { + ThreadContainer(String name, ThreadContainer parent, JSONValue containerObj) { this.name = name; + this.parent = parent; + this.containerObj = containerObj; + } + + /** + * Add a child thread container. + */ + void addChild(ThreadContainer container) { + children.add(container); + } + + /** + * Returns the value of a property of this thread container, as a string. + */ + private String getStringProperty(String propertyName) { + JSONValue value = containerObj.get(propertyName); + return (value != null) ? value.asString() : null; } /** @@ -143,7 +168,10 @@ public String name() { * Return the thread identifier of the owner or empty OptionalLong if not owned. */ public OptionalLong owner() { - return (owner != 0) ? OptionalLong.of(owner) : OptionalLong.empty(); + String owner = getStringProperty("owner"); + return (owner != null) + ? OptionalLong.of(Long.parseLong(owner)) + : OptionalLong.empty(); } /** @@ -164,7 +192,12 @@ public Stream children() { * Returns a stream of {@code ThreadInfo} objects for the threads in this container. */ public Stream threads() { - return threads.stream(); + JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray(); + Set threadInfos = new HashSet<>(); + for (JSONValue threadObj : threadsObj) { + threadInfos.add(new ThreadInfo(threadObj)); + } + return threadInfos.stream(); } /** @@ -176,21 +209,6 @@ public Optional findThread(long tid) { .findAny(); } - /** - * Helper method to recursively find a container with the given name. - */ - ThreadContainer findThreadContainer(String name) { - if (name().equals(name)) - return this; - if (name().startsWith(name + "/")) - return this; - return children() - .map(c -> c.findThreadContainer(name)) - .filter(c -> c != null) - .findAny() - .orElse(null); - } - @Override public int hashCode() { return name.hashCode(); @@ -216,13 +234,30 @@ public String toString() { */ public static final class ThreadInfo { private final long tid; - private final String name; - private final List stack; + private final JSONValue threadObj; - ThreadInfo(long tid, String name, List stack) { - this.tid = tid; - this.name = name; - this.stack = stack; + ThreadInfo(JSONValue threadObj) { + this.tid = Long.parseLong(threadObj.get("tid").asString()); + this.threadObj = threadObj; + } + + /** + * Returns the value of a property of this thread object, as a string. + */ + private String getStringProperty(String propertyName) { + JSONValue value = threadObj.get(propertyName); + return (value != null) ? value.asString() : null; + } + + /** + * Returns the value of a property of an object in this thread object, as a string. + */ + private String getStringProperty(String objectName, String propertyName) { + if (threadObj.get(objectName) instanceof JSONValue.JSONObject obj + && obj.get(propertyName) instanceof JSONValue value) { + return value.asString(); + } + return null; } /** @@ -236,16 +271,96 @@ public long tid() { * Returns the thread name. */ public String name() { - return name; + return getStringProperty("name"); + } + + /** + * Returns the thread state. + */ + public String state() { + return getStringProperty("state"); + } + + /** + * Returns true if virtual thread. + */ + public boolean isVirtual() { + String s = getStringProperty("virtual"); + return (s != null) ? Boolean.parseBoolean(s) : false; + } + + /** + * Returns the thread's parkBlocker. + */ + public String parkBlocker() { + return getStringProperty("parkBlocker", "object"); + } + + /** + * Returns the thread ID of the owner thread if the parkBlocker is an AQS. + */ + public OptionalLong exclusiveOwnerThreadId() { + String s = getStringProperty("parkBlocker", "exclusiveOwnerThreadId"); + return (s != null) + ? OptionalLong.of(Long.parseLong(s)) + : OptionalLong.empty(); + } + + /** + * Returns the object that the thread is blocked entering its monitor. + */ + public String blockedOn() { + return getStringProperty("blockedOn"); + } + + /** + * Return the object that is the therad is waiting on with Object.wait. + */ + public String waitingOn() { + return getStringProperty("waitingOn"); } /** * Returns the thread stack. */ public Stream stack() { + JSONValue.JSONArray stackObj = threadObj.get("stack").asArray(); + List stack = new ArrayList<>(); + for (JSONValue steObject : stackObj) { + stack.add(steObject.asString()); + } return stack.stream(); } + /** + * Return a map of monitors owned. + */ + public Map> ownedMonitors() { + Map> ownedMonitors = new HashMap<>(); + JSONValue monitorsOwnedObj = threadObj.get("monitorsOwned"); + if (monitorsOwnedObj != null) { + for (JSONValue obj : monitorsOwnedObj.asArray()) { + int depth = Integer.parseInt(obj.get("depth").asString()); + for (JSONValue lock : obj.get("locks").asArray()) { + ownedMonitors.computeIfAbsent(depth, _ -> new ArrayList<>()) + .add(lock.asString()); + } + } + } + return ownedMonitors; + } + + /** + * If the thread is a mounted virtual thread, return the thread identifier of + * its carrier. + */ + public OptionalLong carrier() { + String s = getStringProperty("carrier"); + return (s != null) + ? OptionalLong.of(Long.parseLong(s)) + : OptionalLong.empty(); + } + @Override public int hashCode() { return Long.hashCode(tid); @@ -264,6 +379,7 @@ public boolean equals(Object obj) { public String toString() { StringBuilder sb = new StringBuilder("#"); sb.append(tid); + String name = name(); if (name.length() > 0) { sb.append(","); sb.append(name); @@ -273,75 +389,32 @@ public String toString() { } /** - * Parses the given JSON text as a thread dump. + * Returns the value of a property of this thread dump, as a string. */ - private ThreadDump(String json) { - JSONValue threadDumpObj = JSONValue.parse(json).get("threadDump"); - - // maps container name to ThreadContainer - Map map = new HashMap<>(); - - // threadContainers array - JSONValue threadContainersObj = threadDumpObj.get("threadContainers"); - for (JSONValue containerObj : threadContainersObj.asArray()) { - String name = containerObj.get("container").asString(); - String parentName = containerObj.get("parent").asString(); - String owner = containerObj.get("owner").asString(); - JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray(); - - // threads array - Set threadInfos = new HashSet<>(); - for (JSONValue threadObj : threadsObj) { - long tid = Long.parseLong(threadObj.get("tid").asString()); - String threadName = threadObj.get("name").asString(); - JSONValue.JSONArray stackObj = threadObj.get("stack").asArray(); - List stack = new ArrayList<>(); - for (JSONValue steObject : stackObj) { - stack.add(steObject.asString()); - } - threadInfos.add(new ThreadInfo(tid, threadName, stack)); - } - - // add to map if not already encountered - var container = map.computeIfAbsent(name, k -> new ThreadContainer(name)); - if (owner != null) - container.owner = Long.parseLong(owner); - container.threads = threadInfos; - - if (parentName == null) { - rootThreadContainer = container; - } else { - // add parent to map if not already encountered and add to its set of children - var parent = map.computeIfAbsent(parentName, k -> new ThreadContainer(parentName)); - container.parent = parent; - parent.children.add(container); - } - } - - this.processId = Long.parseLong(threadDumpObj.get("processId").asString()); - this.time = threadDumpObj.get("time").asString(); - this.runtimeVersion = threadDumpObj.get("runtimeVersion").asString(); + private String getStringProperty(String propertyName) { + JSONValue value = threadDumpObj.get(propertyName); + return (value != null) ? value.asString() : null; } /** * Returns the value of threadDump/processId. */ public long processId() { - return processId; + return Long.parseLong(getStringProperty("processId")); } /** * Returns the value of threadDump/time. */ public String time() { - return time; + return getStringProperty("time"); } /** * Returns the value of threadDump/runtimeVersion. */ public String runtimeVersion() { - return runtimeVersion; + return getStringProperty("runtimeVersion"); } /** @@ -355,8 +428,17 @@ public ThreadContainer rootThreadContainer() { * Finds a container in the threadDump/threadContainers array with the given name. */ public Optional findThreadContainer(String name) { - ThreadContainer container = rootThreadContainer.findThreadContainer(name); - return Optional.ofNullable(container); + ThreadContainer container = nameToThreadContainer.get(name); + if (container == null) { + // may be name/identity format + container = nameToThreadContainer.entrySet() + .stream() + .filter(e -> e.getKey().startsWith(name + "/")) + .map(e -> e.getValue()) + .findAny() + .orElse(null); + } + return Optional.of(container); } /** @@ -364,6 +446,36 @@ public Optional findThreadContainer(String name) { * @throws RuntimeException if an error occurs */ public static ThreadDump parse(String json) { - return new ThreadDump(json); + JSONValue threadDumpObj = JSONValue.parse(json).get("threadDump"); + + // threadContainers array, preserve insertion order (parents are added before children) + Map containerObjs = new LinkedHashMap<>(); + JSONValue threadContainersObj = threadDumpObj.get("threadContainers"); + for (JSONValue containerObj : threadContainersObj.asArray()) { + String name = containerObj.get("container").asString(); + containerObjs.put(name, containerObj); + } + + // find root and create tree of thread containers + ThreadContainer root = null; + Map map = new HashMap<>(); + for (String name : containerObjs.keySet()) { + JSONValue containerObj = containerObjs.get(name); + String parentName = containerObj.get("parent").asString(); + if (parentName == null) { + root = new ThreadContainer(name, null, containerObj); + map.put(name, root); + } else { + var parent = map.get(parentName); + if (parent == null) { + throw new RuntimeException("Thread container " + name + " found before " + parentName); + } + var container = new ThreadContainer(name, parent, containerObj); + parent.addChild(container); + map.put(name, container); + } + } + + return new ThreadDump(root, map, threadDumpObj); } -} +} \ No newline at end of file From 904fdecb2174072c5e325b1e198565ee7f2c34fa Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Thu, 29 May 2025 10:33:01 +0100 Subject: [PATCH 2/4] Sync up from loom repo, includes review comments --- .../classes/jdk/internal/vm/ThreadDumper.java | 15 +++---------- .../doc-files/threadDump.schema.json | 4 ---- .../HotSpotDiagnosticMXBean/DumpThreads.java | 22 ++++++++++++------- .../jdk/test/lib/threaddump/ThreadDump.java | 14 ++---------- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index 1b5b08331bcf6..96af2e2d2ce51 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -44,7 +44,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.concurrent.locks.AbstractOwnableSynchronizer; /** * Thread dump support. @@ -183,17 +182,12 @@ private static void dumpThread(Thread thread, TextWriter writer) { Instant now = Instant.now(); Thread.State state = snapshot.threadState(); writer.println("#" + thread.threadId() + " \"" + snapshot.threadName() - + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now); + + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now); // park blocker Object parkBlocker = snapshot.parkBlocker(); if (parkBlocker != null) { - writer.print(" // parked on " + Objects.toIdentityString(parkBlocker)); - if (parkBlocker instanceof AbstractOwnableSynchronizer - && snapshot.exclusiveOwnerThread() instanceof Thread owner) { - writer.print(", owned by #" + owner.threadId()); - } - writer.println(); + writer.println(" // parked on " + Objects.toIdentityString(parkBlocker)); } // blocked on monitor enter or Object.wait @@ -317,12 +311,9 @@ private static void dumpThread(Thread thread, JsonWriter jsonWriter) { // park blocker Object parkBlocker = snapshot.parkBlocker(); if (parkBlocker != null) { + // parkBlocker is an object to allow for exclusiveOwnerThread in the future jsonWriter.startObject("parkBlocker"); jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); - if (parkBlocker instanceof AbstractOwnableSynchronizer - && snapshot.exclusiveOwnerThread() instanceof Thread owner) { - jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId()); - } jsonWriter.endObject(); } diff --git a/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json index 711b69f1b0fb0..57ef5c8b859d6 100644 --- a/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json +++ b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json @@ -76,10 +76,6 @@ "object": { "type": "string", "description": "The blocker object responsible for the thread parking." - }, - "exclusiveOwnerThreadId": { - "type": "string", - "description": "The thread identifier of the owner when the blocker object has an owner." } }, "required": [ diff --git a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java index 5a3a4c3bd0d6f..7a830a59733d0 100644 --- a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java +++ b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java @@ -278,7 +278,9 @@ void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception { assertEquals("BLOCKED", ti.state()); assertEquals(lockAsString, ti.blockedOn()); if (pinned) { - assertTrue(ti.carrier().isPresent(), "carrier not found"); + long carrierTid = ti.carrier().orElse(-1L); + assertNotEquals(-1L, carrierTid, "carrier not found"); + assertForkJoinWorkerThread(carrierTid); } } } finally { @@ -348,7 +350,9 @@ void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception { assertEquals(ti.isVirtual(), thread.isVirtual()); assertEquals("WAITING", ti.state()); if (pinned) { - assertTrue(ti.carrier().isPresent(), "carrier not found"); + long carrierTid = ti.carrier().orElse(-1L); + assertNotEquals(-1L, carrierTid, "carrier not found"); + assertForkJoinWorkerThread(carrierTid); } // Compiled native frames have no locals. If Object.wait0 has been compiled @@ -431,15 +435,15 @@ void testParkedThread(ThreadFactory factory, boolean pinned) throws Exception { assertNotNull(ti, "thread not found"); assertEquals(ti.isVirtual(), thread.isVirtual()); - // thread should be waiting on the ReentrantLock, owned by the main thread. + // thread should be waiting on the ReentrantLock assertEquals("WAITING", ti.state()); String parkBlocker = ti.parkBlocker(); assertNotNull(parkBlocker); assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock")); - long ownerTid = ti.exclusiveOwnerThreadId().orElse(-1L); - assertEquals(Thread.currentThread().threadId(), ownerTid); if (pinned) { - assertTrue(ti.carrier().isPresent(), "carrier not found"); + long carrierTid = ti.carrier().orElse(-1L); + assertNotEquals(-1L, carrierTid, "carrier not found"); + assertForkJoinWorkerThread(carrierTid); } } finally { lock.unlock(); @@ -551,7 +555,9 @@ void testMountedVirtualThread() throws Exception { .orElse(null); assertNotNull(ti, "thread not found"); assertTrue(ti.isVirtual()); - assertTrue(ti.carrier().isPresent(), "carrier not found"); + long carrierTid = ti.carrier().orElse(-1L); + assertNotEquals(-1L, carrierTid, "carrier not found"); + assertForkJoinWorkerThread(carrierTid); } finally { done.set(true); thread.join(); @@ -561,7 +567,7 @@ void testMountedVirtualThread() throws Exception { /** * Asserts that the given thread identifier is a ForkJoinWorkerThread. */ - void assertCarrier(long tid) { + private void assertForkJoinWorkerThread(long tid) { Thread thread = Thread.getAllStackTraces() .keySet() .stream() diff --git a/test/lib/jdk/test/lib/threaddump/ThreadDump.java b/test/lib/jdk/test/lib/threaddump/ThreadDump.java index d12419e687d95..f4964a9521f70 100644 --- a/test/lib/jdk/test/lib/threaddump/ThreadDump.java +++ b/test/lib/jdk/test/lib/threaddump/ThreadDump.java @@ -296,16 +296,6 @@ public String parkBlocker() { return getStringProperty("parkBlocker", "object"); } - /** - * Returns the thread ID of the owner thread if the parkBlocker is an AQS. - */ - public OptionalLong exclusiveOwnerThreadId() { - String s = getStringProperty("parkBlocker", "exclusiveOwnerThreadId"); - return (s != null) - ? OptionalLong.of(Long.parseLong(s)) - : OptionalLong.empty(); - } - /** * Returns the object that the thread is blocked entering its monitor. */ @@ -381,8 +371,8 @@ public String toString() { sb.append(tid); String name = name(); if (name.length() > 0) { - sb.append(","); - sb.append(name); + sb.append(",") + .append(name); } return sb.toString(); } From 04f559fbbaba23e67315b30255e9d1fb109e6124 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Thu, 29 May 2025 10:33:25 +0100 Subject: [PATCH 3/4] Temp fixed until fixed in pull/25425 --- src/hotspot/share/services/threadService.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hotspot/share/services/threadService.cpp b/src/hotspot/share/services/threadService.cpp index 7d35b2de078e7..a8b0ae7a55c61 100644 --- a/src/hotspot/share/services/threadService.cpp +++ b/src/hotspot/share/services/threadService.cpp @@ -1293,7 +1293,7 @@ class GetThreadSnapshotClosure: public HandshakeClosure { return; } - bool walk_cont = (_java_thread != nullptr) && (_java_thread->vthread_continuation() != nullptr); + bool vthread_carrier = !is_virtual && (_java_thread != nullptr) && (_java_thread->vthread_continuation() != nullptr); oop park_blocker = java_lang_Thread::park_blocker(_thread_h()); if (park_blocker != nullptr) { @@ -1314,7 +1314,7 @@ class GetThreadSnapshotClosure: public HandshakeClosure { int total_count = 0; vframeStream vfst(_java_thread != nullptr - ? vframeStream(_java_thread, false, true, walk_cont) + ? vframeStream(_java_thread, false, true, vthread_carrier) : vframeStream(java_lang_VirtualThread::continuation(_thread_h()))); for (; From 160ca36cd7b898de5dbe22a2a4f961cd343ac343 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Sat, 31 May 2025 09:03:29 +0100 Subject: [PATCH 4/4] Sync up from loom repo, includes review comments --- .../classes/jdk/internal/vm/ThreadDumper.java | 54 +++++++++++-------- .../jdk/internal/vm/ThreadSnapshot.java | 4 ++ .../HotSpotDiagnosticMXBean/DumpThreads.java | 8 +-- .../DumpThreadsWithEliminatedLock.java | 2 +- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index 96af2e2d2ce51..58729774f2406 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -93,8 +93,8 @@ public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) { /** * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded. * This method is the implementation of the Thread.dump_to_file diagnostic command - * when a file path is not specified. It returns the thread and/or message to send - * to the tool user. + * when a file path is not specified. It returns the thread dump and/or message to + * send to the tool user. */ private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { var out = new BoundedByteArrayOutputStream(maxSize); @@ -117,7 +117,7 @@ private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { /** * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded. * This method is the implementation of the Thread.dump_to_file diagnostic command. - * It returns the thread and/or message to send to the tool user. + * It returns the thread dump and/or message to send to the tool user. */ private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) { Path path = Path.of(file).toAbsolutePath(); @@ -184,37 +184,49 @@ private static void dumpThread(Thread thread, TextWriter writer) { writer.println("#" + thread.threadId() + " \"" + snapshot.threadName() + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now); - // park blocker - Object parkBlocker = snapshot.parkBlocker(); - if (parkBlocker != null) { - writer.println(" // parked on " + Objects.toIdentityString(parkBlocker)); - } - - // blocked on monitor enter or Object.wait - if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) { - writer.println(" // blocked on " + Objects.toIdentityString(obj)); - } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) - && snapshot.waitingOn() instanceof Object obj) { - writer.println(" // waiting on " + Objects.toIdentityString(obj)); - } - StackTraceElement[] stackTrace = snapshot.stackTrace(); int depth = 0; while (depth < stackTrace.length) { + writer.print(" at "); + writer.println(stackTrace[depth]); snapshot.ownedMonitorsAt(depth).forEach(o -> { if (o != null) { - writer.println(" // locked " + Objects.toIdentityString(o)); + writer.println(" - locked " + decorateObject(o)); } else { - writer.println(" // lock is eliminated"); + writer.println(" - lock is eliminated"); } }); - writer.print(" "); - writer.println(stackTrace[depth]); + + // if parkBlocker set, or blocked/waiting on monitor, then print after top frame + if (depth == 0) { + // park blocker + Object parkBlocker = snapshot.parkBlocker(); + if (parkBlocker != null) { + writer.println(" - parking to wait for " + decorateObject(parkBlocker)); + } + + // blocked on monitor enter or Object.wait + if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) { + writer.println(" - waiting to lock " + decorateObject(obj)); + } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) + && snapshot.waitingOn() instanceof Object obj) { + writer.println(" - waiting on " + decorateObject(obj)); + } + } + depth++; } writer.println(); } + /** + * Returns the identity string for the given object in a form suitable for the plain + * text format thread dump. + */ + private static String decorateObject(Object obj) { + return "<" + Objects.toIdentityString(obj) + ">"; + } + /** * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded. * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java index 205bfde5449b3..b5607059abc7d 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java @@ -52,9 +52,13 @@ private ThreadSnapshot() {} /** * Take a snapshot of a Thread to get all information about the thread. + * @throws UnsupportedOperationException if not supported by VM */ static ThreadSnapshot of(Thread thread) { ThreadSnapshot snapshot = create(thread); + if (snapshot == null) { + throw new UnsupportedOperationException(); + } if (snapshot.stackTrace == null) { snapshot.stackTrace = EMPTY_STACK; } diff --git a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java index 7a830a59733d0..adf643749c7e8 100644 --- a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java +++ b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java @@ -267,7 +267,7 @@ void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception { ThreadFields fields = findThread(tid, lines); assertNotNull(fields, "thread not found"); assertEquals("BLOCKED", fields.state()); - assertTrue(contains(lines, "// blocked on " + lockAsString)); + assertTrue(contains(lines, "- waiting to lock <" + lockAsString)); // thread dump in JSON format should include thread in root container ThreadDump threadDump = dumpThreadsToJson(); @@ -361,7 +361,7 @@ void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception { boolean expectWaitingOn = !WhiteBox.getWhiteBox().isMethodCompiled(wait0); if (expectWaitingOn) { // plain text dump should have "waiting on" line - assertTrue(contains(lines, "// waiting on " + lockAsString)); + assertTrue(contains(lines, "- waiting on <" + lockAsString)); // JSON thread dump should have waitingOn property assertEquals(lockAsString, ti.waitingOn()); @@ -425,7 +425,7 @@ void testParkedThread(ThreadFactory factory, boolean pinned) throws Exception { ThreadFields fields = findThread(tid, lines); assertNotNull(fields, "thread not found"); assertEquals("WAITING", fields.state()); - assertTrue(contains(lines, "// parked on java.util.concurrent.locks.ReentrantLock")); + assertTrue(contains(lines, "- parking to wait for stream = Files.lines(file)) { found = stream.map(String::trim) - .anyMatch(l -> l.contains("// lock is eliminated")); + .anyMatch(l -> l.contains("- lock is eliminated")); } System.out.format("%s Attempt %d, found: %b%n", Instant.now(), attempts, found); }