8356870: HotSpotDiagnosticMXBean.dumpThreads and jcmd Thread.dump_to_file updates

Reviewed-by: sspitsyn, kevinw
This commit is contained in:
Alan Bateman 2025-06-04 04:10:10 +00:00
parent ebd85288ce
commit f17b2bc06a
9 changed files with 1565 additions and 439 deletions

View File

@ -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,19 @@ import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* Thread dump support.
*
* This class defines methods to dump threads to an output stream or file in plain
* 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 +59,12 @@ public class 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 +75,12 @@ public class ThreadDumper {
}
/**
* 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 +92,32 @@ public class ThreadDumper {
/**
* 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 dump 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 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();
@ -110,224 +125,412 @@ public class ThreadDumper {
? 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);
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 " + decorateObject(o));
} else {
writer.println(" - lock is eliminated");
}
});
// 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++;
}
ps.println();
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.
* @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\": {");
private static void dumpThreadsToJson(TextWriter textWriter) {
var jsonWriter = new JsonWriter(textWriter);
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));
jsonWriter.startObject(); // top-level object
out.println(" \"threadContainers\": [");
List<ThreadContainer> containers = allContainers();
Iterator<ThreadContainer> iterator = containers.iterator();
while (iterator.hasNext()) {
ThreadContainer container = iterator.next();
boolean more = iterator.hasNext();
dumpThreadsToJson(container, out, more);
}
out.println(" ]"); // end of threadContainers
jsonWriter.startObject("threadDump");
out.println(" }"); // end threadDump
out.println("}"); // end object
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<Thread> 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\": [");
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();
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
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) {
// parkBlocker is an object to allow for exclusiveOwnerThread in the future
jsonWriter.startObject("parkBlocker");
jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
jsonWriter.endObject();
}
// 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));
}
// stack trace
jsonWriter.startArray("stack");
Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
jsonWriter.endArray();
// monitors owned, skip if none
if (snapshot.ownsMonitors()) {
jsonWriter.startArray("monitorsOwned");
int depth = 0;
while (depth < stackTrace.length) {
List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
if (!objs.isEmpty()) {
jsonWriter.startObject();
jsonWriter.writeProperty("depth", depth);
jsonWriter.startArray("locks");
snapshot.ownedMonitorsAt(depth)
.map(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();
}
/**
* 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 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;
}
}
out.println(" ]");
if (more) {
out.println(" },");
} else {
out.println(" }"); // last thread, no trailing comma
private final Deque<Node> stack = new ArrayDeque<>();
private final TextWriter writer;
JsonWriter(TextWriter writer) {
this.writer = writer;
}
}
/**
* Returns a list of all thread containers that are "reachable" from
* the root container.
*/
private static List<ThreadContainer> allContainers() {
List<ThreadContainer> containers = new ArrayList<>();
collect(ThreadContainers.root(), containers);
return containers;
}
private void indent() {
int indent = stack.size() * 2;
writer.print(" ".repeat(indent));
}
private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
containers.add(container);
container.children().forEach(c -> collect(c, containers));
}
/**
* 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));
}
/**
* 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);
/**
* 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 +560,56 @@ public class ThreadDumper {
}
}
/**
* 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.
*/

View File

@ -52,9 +52,13 @@ class 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;
}

View File

@ -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.
*
* <p> When the format is specified as {@link ThreadDumpFormat#JSON JSON}, the
* thread dump is generated in JavaScript Object Notation.
* <a href="doc-files/threadDump.schema.json">threadDump.schema.json</a>
* describes the thread dump format in draft
* <a href="https://tools.ietf.org/html/draft-json-schema-language-02">
* JSON Schema Language version 2</a>.
*
* <p> 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 interface HotSpotDiagnosticMXBean extends PlatformManagedObject {
TEXT_PLAIN,
/**
* JSON (JavaScript Object Notation) format.
* @spec https://datatracker.ietf.org/doc/html/rfc8259 JavaScript Object Notation
*/
JSON,
}

View File

@ -0,0 +1,171 @@
{
"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."
}
},
"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"
]
}

View File

@ -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 class HotSpotDiagnostic implements HotSpotDiagnosticMXBean {
throw new IllegalArgumentException("'outputFile' not absolute path");
try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW)) {
dumpThreads(out, format);
dumpThreads(out, format);
}
}

View File

@ -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 @@ class ThreadDumpToFileTest {
@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 @@ class ThreadDumpToFileTest {
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 @@ class ThreadDumpToFileTest {
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");
}
/**

View File

@ -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<String> 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,438 @@ class DumpThreads {
}
/**
* Test thread dump in plain text format contains information about the current
* thread and a virtual thread created directly with the Thread API.
*/
@Test
void testRootContainerPlainTextFormat() throws Exception {
Thread vthread = Thread.ofVirtual().start(LockSupport::park);
try {
testDumpThreadsPlainText(vthread, trackAllThreads);
} finally {
LockSupport.unpark(vthread);
}
}
/**
* Test thread dump in JSON format contains information about the current
* thread and a virtual thread created directly with the Thread API.
*/
@Test
void testRootContainerJsonFormat() throws Exception {
Thread vthread = Thread.ofVirtual().start(LockSupport::park);
try {
testDumpThreadsJson(null, vthread, trackAllThreads);
} finally {
LockSupport.unpark(vthread);
}
}
/**
* Test thread dump in plain text format includes a thread executing a task in the
* given ExecutorService.
* Test that a thread container for an executor service is in the JSON format thread dump.
*/
@ParameterizedTest
@MethodSource("executors")
void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception {
void testThreadContainer(ExecutorService executor) throws Exception {
try (executor) {
Thread thread = forkParker(executor);
try {
testDumpThreadsPlainText(thread, true);
} finally {
LockSupport.unpark(thread);
}
testThreadContainer(executor, Objects.toIdentityString(executor));
}
}
/**
* Test thread dump in JSON format includes a thread executing a task in the
* given ExecutorService.
*/
@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);
}
}
}
/**
* Test thread dump in JSON format includes a thread executing a task in the
* fork-join common pool.
* Test that a thread container for the common pool is in the JSON format thread dump.
*/
@Test
void testForkJoinPool() throws Exception {
ForkJoinPool pool = ForkJoinPool.commonPool();
Thread thread = forkParker(pool);
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<Thread>();
executor.submit(() -> {
threadRef.set(Thread.currentThread());
LockSupport.park();
});
// capture Thread
Thread thread;
while ((thread = threadRef.get()) == null) {
Thread.sleep(20);
}
try {
testDumpThreadsJson("ForkJoinPool.commonPool", thread, true);
// 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(thread);
}
}
/**
* 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
* ThreadFactory implementations for tests.
*/
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);
// pid should be on the first line
String line1 = line(file, 0);
String pid = Long.toString(ProcessHandle.current().pid());
assertTrue(line1.contains(pid));
// timestamp should be on the second line
String line2 = line(file, 1);
ZonedDateTime.parse(line2);
// runtime version should be on third line
String line3 = line(file, 2);
String vs = Runtime.version().toString();
assertTrue(line3.contains(vs));
// test if thread is included in thread dump
assertEquals(expectInDump, isPresent(file, thread));
// 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));
static Stream<ThreadFactory> threadFactories() {
Stream<ThreadFactory> s = Stream.of(Thread.ofPlatform().factory());
if (trackAllThreads) {
return Stream.concat(s, Stream.of(Thread.ofVirtual().factory()));
} else {
return s;
}
}
/**
* 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 blocked on monitor enter.
*/
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 testBlockedThread(ThreadFactory factory) throws Exception {
testBlockedThread(factory, false);
}
// parse the JSON text
String jsonText = Files.readString(file);
ThreadDump threadDump = ThreadDump.parse(jsonText);
/**
* Test thread dump with a thread blocked on monitor enter when pinned.
*/
@Test
void testBlockedThreadWhenPinned() throws Exception {
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
testBlockedThread(Thread.ofVirtual().factory(), true);
}
// test threadDump/processId
assertTrue(threadDump.processId() == ProcessHandle.current().pid());
void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception {
var lock = new Object();
var started = new CountDownLatch(1);
// test threadDump/time can be parsed
ZonedDateTime.parse(threadDump.time());
Thread thread = factory.newThread(() -> {
if (pinned) {
VThreadPinner.runPinned(() -> {
started.countDown();
synchronized (lock) { } // blocks
});
} else {
started.countDown();
synchronized (lock) { } // blocks
}
});
// test threadDump/runtimeVersion
assertEquals(Runtime.version().toString(), threadDump.runtimeVersion());
try {
synchronized (lock) {
// start thread and wait for it to block
thread.start();
started.await();
await(thread, Thread.State.BLOCKED);
// test root container, has no parent and no owner
var rootContainer = threadDump.rootThreadContainer();
assertFalse(rootContainer.owner().isPresent());
assertFalse(rootContainer.parent().isPresent());
long tid = thread.threadId();
String lockAsString = Objects.toIdentityString(lock);
// 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);
// thread dump in plain text should include thread
List<String> lines = dumpThreadsToPlainText();
ThreadFields fields = findThread(tid, lines);
assertNotNull(fields, "thread not found");
assertEquals("BLOCKED", fields.state());
assertTrue(contains(lines, "- waiting to lock <" + 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) {
long carrierTid = ti.carrier().orElse(-1L);
assertNotEquals(-1L, carrierTid, "carrier not found");
assertForkJoinWorkerThread(carrierTid);
}
}
} finally {
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 thread dump with a thread waiting in Object.wait.
*/
@ParameterizedTest
@MethodSource("threadFactories")
void testWaitingThread(ThreadFactory factory) throws Exception {
testWaitingThread(factory, false);
}
/**
* 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);
}
void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception {
var lock = new Object();
var started = new CountDownLatch(1);
Thread thread = factory.newThread(() -> {
try {
synchronized (lock) {
if (pinned) {
VThreadPinner.runPinned(() -> {
started.countDown();
lock.wait();
});
} else {
started.countDown();
lock.wait();
}
}
} catch (InterruptedException e) { }
});
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<String> 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) {
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
// 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();
}
}
/**
* Test thread dump with a thread parked on a j.u.c. lock.
*/
@ParameterizedTest
@MethodSource("threadFactories")
void testParkedThread(ThreadFactory factory) throws Exception {
testParkedThread(factory, false);
}
/**
* 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);
}
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();
}
});
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<String> lines = dumpThreadsToPlainText();
ThreadFields fields = findThread(tid, lines);
assertNotNull(fields, "thread not found");
assertEquals("WAITING", fields.state());
assertTrue(contains(lines, "- parking to wait for <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
assertEquals("WAITING", ti.state());
String parkBlocker = ti.parkBlocker();
assertNotNull(parkBlocker);
assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
if (pinned) {
long carrierTid = ti.carrier().orElse(-1L);
assertNotEquals(-1L, carrierTid, "carrier not found");
assertForkJoinWorkerThread(carrierTid);
}
} finally {
lock.unlock();
thread.join();
}
}
/**
* Test thread dump with a thread owning a monitor.
*/
@ParameterizedTest
@MethodSource("threadFactories")
void testThreadOwnsMonitor(ThreadFactory factory) throws Exception {
testThreadOwnsMonitor(factory, false);
}
@Test
void testThreadOwnsMonitorWhenPinned() throws Exception {
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
testThreadOwnsMonitor(Thread.ofVirtual().factory(), true);
}
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<String> 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<String> ownedMonitors = ti.ownedMonitors().values()
.stream()
.flatMap(List::stream)
.collect(Collectors.toSet());
assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
} finally {
LockSupport.unpark(thread);
thread.join();
}
}
/**
* 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<String> 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());
long carrierTid = ti.carrier().orElse(-1L);
assertNotEquals(-1L, carrierTid, "carrier not found");
assertForkJoinWorkerThread(carrierTid);
} finally {
done.set(true);
thread.join();
}
}
/**
* Asserts that the given thread identifier is a ForkJoinWorkerThread.
*/
private void assertForkJoinWorkerThread(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 +591,44 @@ class DumpThreads {
() -> 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 +640,7 @@ class DumpThreads {
* 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 +649,63 @@ class DumpThreads {
}
/**
* 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<String> 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<String> 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<String> 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<String> 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 +719,23 @@ class DumpThreads {
}
/**
* 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<String> 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<String> stream = Files.lines(file)) {
return stream.skip(n).findFirst().orElseThrow();
private static void awaitTrue(AtomicBoolean ref) throws Exception {
while (!ref.get()) {
Thread.sleep(20);
}
}
}

View File

@ -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<String>();
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<String> 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;
}
}

View File

@ -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 @@ package jdk.test.lib.threaddump;
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 @@ import jdk.test.lib.json.JSONValue;
* {
* "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 @@ import jdk.test.lib.json.JSONValue;
* }</pre>
*/
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<String, ThreadContainer> nameToThreadContainer;
private final JSONValue threadDumpObj;
private ThreadDump(ThreadContainer rootThreadContainer,
Map<String, ThreadContainer> 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<ThreadInfo> threads;
private final ThreadContainer parent;
private final Set<ThreadContainer> 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 final class ThreadDump {
* 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 final class ThreadDump {
* Returns a stream of {@code ThreadInfo} objects for the threads in this container.
*/
public Stream<ThreadInfo> threads() {
return threads.stream();
JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray();
Set<ThreadInfo> threadInfos = new HashSet<>();
for (JSONValue threadObj : threadsObj) {
threadInfos.add(new ThreadInfo(threadObj));
}
return threadInfos.stream();
}
/**
@ -176,21 +209,6 @@ public final class ThreadDump {
.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 final class ThreadDump {
*/
public static final class ThreadInfo {
private final long tid;
private final String name;
private final List<String> stack;
private final JSONValue threadObj;
ThreadInfo(long tid, String name, List<String> 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,86 @@ public final class ThreadDump {
* 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 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<String> stack() {
JSONValue.JSONArray stackObj = threadObj.get("stack").asArray();
List<String> stack = new ArrayList<>();
for (JSONValue steObject : stackObj) {
stack.add(steObject.asString());
}
return stack.stream();
}
/**
* Return a map of monitors owned.
*/
public Map<Integer, List<String>> ownedMonitors() {
Map<Integer, List<String>> 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,84 +369,42 @@ public final class ThreadDump {
public String toString() {
StringBuilder sb = new StringBuilder("#");
sb.append(tid);
String name = name();
if (name.length() > 0) {
sb.append(",");
sb.append(name);
sb.append(",")
.append(name);
}
return sb.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<String, ThreadContainer> 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<ThreadInfo> 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<String> 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 +418,17 @@ public final class ThreadDump {
* Finds a container in the threadDump/threadContainers array with the given name.
*/
public Optional<ThreadContainer> 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 +436,36 @@ public final class ThreadDump {
* @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<String, JSONValue> 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<String, ThreadContainer> 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);
}
}