blob: a26003a3afbe468196b543e3e4709aea445e8c95 [file]
/*
* 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
package jdk.internal.vm;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
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;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
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 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() { }
// the maximum byte array to return when generating the thread dump to a byte array
private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
/**
* 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 tool user
*/
public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
} else {
return dumpThreadsToFile(file, okayToOverwrite, false);
}
}
/**
* 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 tool user
*/
public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
} else {
return dumpThreadsToFile(file, okayToOverwrite, true);
}
}
/**
* 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) {
var out = new BoundedByteArrayOutputStream(maxSize);
try (out; var writer = new TextWriter(out)) {
if (json) {
dumpThreadsToJson(writer);
} else {
dumpThreads(writer);
}
} 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();
OpenOption[] options = (okayToOverwrite)
? new OpenOption[0]
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
String reply;
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());
}
} catch (FileAlreadyExistsException _) {
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
} 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.
* @throws IOException if an I/O error occurs
*/
public static void dumpThreads(OutputStream out) throws IOException {
var writer = new TextWriter(out);
try {
dumpThreads(writer);
writer.flush();
} catch (UncheckedIOException e) {
IOException ioe = e.getCause();
throw ioe;
}
}
/**
* 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(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, TextWriter writer) {
container.threads().forEach(t -> dumpThread(t, writer));
container.children().forEach(c -> dumpThreads(c, writer));
}
private static boolean dumpThread(Thread thread, TextWriter writer) {
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
if (snapshot == null) {
return false; // thread terminated
}
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++;
}
writer.println();
return true;
}
/**
* 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) throws IOException {
var writer = new TextWriter(out);
try {
dumpThreadsToJson(writer);
writer.flush();
} catch (UncheckedIOException e) {
IOException ioe = e.getCause();
throw ioe;
}
}
/**
* Generate a thread dump to the given text stream in JSON format.
* @throws UncheckedIOException if an I/O error occurs
*/
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
}
/**
* Write a thread container to the given JSON writer.
* @throws UncheckedIOException if an I/O error occurs
*/
private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
jsonWriter.startObject();
jsonWriter.writeProperty("container", container);
jsonWriter.writeProperty("parent", container.parent());
Thread owner = container.owner();
jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
long threadCount = 0;
jsonWriter.startArray("threads");
Iterator<Thread> threads = container.threads().iterator();
while (threads.hasNext()) {
Thread thread = threads.next();
if (dumpThread(thread, jsonWriter)) {
threadCount++;
}
}
jsonWriter.endArray(); // threads
// thread count
if (!ThreadContainers.trackAllThreads()) {
threadCount = Long.max(threadCount, container.threadCount());
}
jsonWriter.writeProperty("threadCount", threadCount);
jsonWriter.endObject();
// the children of the thread container follow
container.children().forEach(c -> dumpThreads(c, jsonWriter));
}
/**
* Write a thread to the given JSON writer.
* @return true if the thread dump was written, false otherwise
* @throws UncheckedIOException if an I/O error occurs
*/
private static boolean dumpThread(Thread thread, JsonWriter jsonWriter) {
Instant now = Instant.now();
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
if (snapshot == null) {
return false; // thread terminated
}
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) {
// 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();
return true;
}
/**
* 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;
}
}
private final Deque<Node> 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();
}
}
/**
* A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
* written the subsequent bytes are discarded.
*/
private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
final int max;
BoundedByteArrayOutputStream(int max) {
this.max = max;
}
@Override
public void write(int b) {
if (max < count) {
super.write(b);
}
}
@Override
public void write(byte[] b, int off, int len) {
int remaining = max - count;
if (remaining > 0) {
super.write(b, off, Integer.min(len, remaining));
}
}
@Override
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.
*/
private static long processId() {
try {
return ProcessHandle.current().pid();
} catch (UnsupportedOperationException e) {
return -1L;
}
}
}