1 /*
2 * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25 package jdk.internal.vm;
26
27 import java.io.BufferedWriter;
28 import java.io.ByteArrayOutputStream;
29 import java.io.IOException;
30 import java.io.OutputStream;
31 import java.io.OutputStreamWriter;
32 import java.io.UncheckedIOException;
33 import java.io.Writer;
34 import java.nio.charset.StandardCharsets;
35 import java.nio.file.FileAlreadyExistsException;
36 import java.nio.file.Files;
37 import java.nio.file.OpenOption;
38 import java.nio.file.Path;
39 import java.nio.file.StandardOpenOption;
40 import java.time.Instant;
41 import java.util.ArrayDeque;
42 import java.util.Arrays;
43 import java.util.Deque;
44 import java.util.Iterator;
45 import java.util.List;
46 import java.util.Objects;
47
48 /**
49 * Thread dump support.
50 *
51 * This class defines static methods to support the Thread.dump_to_file diagnostic command
52 * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
53 * thread dump to a file or byte array in plain text or JSON format.
54 */
55 public class ThreadDumper {
56 private ThreadDumper() { }
57
58 // the maximum byte array to return when generating the thread dump to a byte array
59 private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
60
61 /**
62 * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.
63 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
64 *
65 * @param file the file path to the file, null or "-" to return a byte array
66 * @param okayToOverwrite true to overwrite an existing file
67 * @return the UTF-8 encoded thread dump or message to return to the tool user
68 */
69 public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
70 if (file == null || file.equals("-")) {
71 return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
72 } else {
73 return dumpThreadsToFile(file, okayToOverwrite, false);
74 }
75 }
76
77 /**
78 * Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded.
79 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
80 *
81 * @param file the file path to the file, null or "-" to return a byte array
82 * @param okayToOverwrite true to overwrite an existing file
83 * @return the UTF-8 encoded thread dump or message to return to the tool user
84 */
85 public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
86 if (file == null || file.equals("-")) {
87 return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
88 } else {
89 return dumpThreadsToFile(file, okayToOverwrite, true);
90 }
91 }
92
93 /**
94 * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
95 * This method is the implementation of the Thread.dump_to_file diagnostic command
96 * when a file path is not specified. It returns the thread dump and/or message to
97 * send to the tool user.
98 */
99 private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
100 var out = new BoundedByteArrayOutputStream(maxSize);
101 try (out; var writer = new TextWriter(out)) {
102 if (json) {
103 dumpThreadsToJson(writer);
104 } else {
105 dumpThreads(writer);
106 }
107 } catch (Exception ex) {
108 if (ex instanceof UncheckedIOException ioe) {
109 ex = ioe.getCause();
110 }
111 String reply = String.format("Failed: %s%n", ex);
112 return reply.getBytes(StandardCharsets.UTF_8);
113 }
114 return out.toByteArray();
115 }
116
117 /**
118 * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
119 * This method is the implementation of the Thread.dump_to_file diagnostic command.
120 * It returns the thread dump and/or message to send to the tool user.
121 */
122 private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
123 Path path = Path.of(file).toAbsolutePath();
124 OpenOption[] options = (okayToOverwrite)
125 ? new OpenOption[0]
126 : new OpenOption[] { StandardOpenOption.CREATE_NEW };
127 String reply;
128 try (OutputStream out = Files.newOutputStream(path, options)) {
129 try (var writer = new TextWriter(out)) {
130 if (json) {
131 dumpThreadsToJson(writer);
132 } else {
133 dumpThreads(writer);
134 }
135 reply = String.format("Created %s%n", path);
136 } catch (UncheckedIOException e) {
137 reply = String.format("Failed: %s%n", e.getCause());
138 }
139 } catch (FileAlreadyExistsException _) {
140 reply = String.format("%s exists, use -overwrite to overwrite%n", path);
141 } catch (Exception ex) {
142 reply = String.format("Failed: %s%n", ex);
143 }
144 return reply.getBytes(StandardCharsets.UTF_8);
145 }
146
147 /**
148 * Generate a thread dump in plain text format to the given output stream, UTF-8
149 * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
150 * @throws IOException if an I/O error occurs
151 */
152 public static void dumpThreads(OutputStream out) throws IOException {
153 var writer = new TextWriter(out);
154 try {
155 dumpThreads(writer);
156 writer.flush();
157 } catch (UncheckedIOException e) {
158 IOException ioe = e.getCause();
159 throw ioe;
160 }
161 }
162
163 /**
164 * Generate a thread dump in plain text format to the given text stream.
165 * @throws UncheckedIOException if an I/O error occurs
166 */
167 private static void dumpThreads(TextWriter writer) {
168 writer.println(processId());
169 writer.println(Instant.now());
170 writer.println(Runtime.version());
171 writer.println();
172 dumpThreads(ThreadContainers.root(), writer);
173 }
174
175 private static void dumpThreads(ThreadContainer container, TextWriter writer) {
176 container.threads().forEach(t -> dumpThread(t, writer));
177 container.children().forEach(c -> dumpThreads(c, writer));
178 }
179
180 private static boolean dumpThread(Thread thread, TextWriter writer) {
181 ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
182 if (snapshot == null) {
183 return false; // thread terminated
184 }
185 Instant now = Instant.now();
186 Thread.State state = snapshot.threadState();
187 writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
188 + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now);
189
190 StackTraceElement[] stackTrace = snapshot.stackTrace();
191 int depth = 0;
192 while (depth < stackTrace.length) {
193 writer.print(" at ");
194 writer.println(stackTrace[depth]);
195 snapshot.ownedMonitorsAt(depth).forEach(o -> {
196 if (o != null) {
197 writer.println(" - locked " + decorateObject(o));
198 } else {
199 writer.println(" - lock is eliminated");
200 }
201 });
202
203 // if parkBlocker set, or blocked/waiting on monitor, then print after top frame
204 if (depth == 0) {
205 // park blocker
206 Object parkBlocker = snapshot.parkBlocker();
207 if (parkBlocker != null) {
208 String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner)
209 ? ", owner #" + owner.threadId()
210 : "";
211 writer.println(" - parking to wait for " + decorateObject(parkBlocker) + suffix);
212 }
213
214 // blocked on monitor enter or Object.wait
215 if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
216 writer.println(" - waiting to lock " + decorateObject(obj));
217 } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
218 && snapshot.waitingOn() instanceof Object obj) {
219 writer.println(" - waiting on " + decorateObject(obj));
220 }
221 }
222
223 depth++;
224 }
225 writer.println();
226 return true;
227 }
228
229 /**
230 * Returns the identity string for the given object in a form suitable for the plain
231 * text format thread dump.
232 */
233 private static String decorateObject(Object obj) {
234 return "<" + Objects.toIdentityString(obj) + ">";
235 }
236
237 /**
238 * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
239 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
240 * @throws IOException if an I/O error occurs
241 */
242 public static void dumpThreadsToJson(OutputStream out) throws IOException {
243 var writer = new TextWriter(out);
244 try {
245 dumpThreadsToJson(writer);
246 writer.flush();
247 } catch (UncheckedIOException e) {
248 IOException ioe = e.getCause();
249 throw ioe;
250 }
251 }
252
253 /**
254 * JSON is schema-less and the thread dump format will evolve over time.
255 * {@code HotSpotDiagnosticMXBean.dumpThreads} links to a JSON file that documents
256 * the latest/current format. A system property can be used to generate the thread
257 * dump in older formats if necessary.
258 */
259 private static class JsonFormat {
260 private static final String JSON_FORMAT_VERSION_PROP =
261 "com.sun.management.HotSpotDiagnosticMXBean.dumpThreads.format";
262 static final int JSON_FORMAT_V1 = 1;
263 static final int JSON_FORMAT_V2 = 2;
264 private static final int JSON_FORMAT_LATEST = JSON_FORMAT_V2;
265 private static final int JSON_FORMAT;
266 static {
267 int ver = Integer.getInteger(JSON_FORMAT_VERSION_PROP, JSON_FORMAT_LATEST);
268 JSON_FORMAT = Math.clamp(ver, JSON_FORMAT_V1, JSON_FORMAT_LATEST);
269 }
270
271 static int formatVersion() {
272 return JSON_FORMAT;
273 }
274 }
275
276 /**
277 * Generate a thread dump to the given text stream in JSON format.
278 * @throws UncheckedIOException if an I/O error occurs
279 */
280 private static void dumpThreadsToJson(TextWriter textWriter) {
281 int format = JsonFormat.formatVersion();
282 var jsonWriter = new JsonWriter(textWriter, (format == JsonFormat.JSON_FORMAT_V1));
283 jsonWriter.startObject(); // top-level object
284 jsonWriter.startObject("threadDump");
285 if (format > JsonFormat.JSON_FORMAT_V1) {
286 jsonWriter.writeProperty("formatVersion", format);
287 }
288
289 jsonWriter.writeLongProperty("processId", processId());
290 jsonWriter.writeProperty("time", Instant.now());
291 jsonWriter.writeProperty("runtimeVersion", Runtime.version());
292
293 jsonWriter.startArray("threadContainers");
294 dumpThreads(ThreadContainers.root(), jsonWriter);
295 jsonWriter.endArray();
296
297 jsonWriter.endObject(); // threadDump
298
299 jsonWriter.endObject(); // end of top-level object
300 }
301
302 /**
303 * Write a thread container to the given JSON writer.
304 * @throws UncheckedIOException if an I/O error occurs
305 */
306 private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
307 jsonWriter.startObject();
308 jsonWriter.writeProperty("container", container);
309 jsonWriter.writeProperty("parent", container.parent());
310
311 Thread owner = container.owner();
312 if (owner != null) {
313 jsonWriter.writeLongProperty("owner", owner.threadId());
314 } else {
315 jsonWriter.writeProperty("owner", null); // owner is not optional
316 }
317
318 long threadCount = 0;
319 jsonWriter.startArray("threads");
320 Iterator<Thread> threads = container.threads().iterator();
321 while (threads.hasNext()) {
322 Thread thread = threads.next();
323 if (dumpThread(thread, jsonWriter)) {
324 threadCount++;
325 }
326 }
327 jsonWriter.endArray(); // threads
328
329 // thread count
330 if (!ThreadContainers.trackAllThreads()) {
331 threadCount = Long.max(threadCount, container.threadCount());
332 }
333 jsonWriter.writeLongProperty("threadCount", threadCount);
334
335 jsonWriter.endObject();
336
337 // the children of the thread container follow
338 container.children().forEach(c -> dumpThreads(c, jsonWriter));
339 }
340
341 /**
342 * Write a thread to the given JSON writer.
343 * @return true if the thread dump was written, false otherwise
344 * @throws UncheckedIOException if an I/O error occurs
345 */
346 private static boolean dumpThread(Thread thread, JsonWriter jsonWriter) {
347 Instant now = Instant.now();
348 ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
349 if (snapshot == null) {
350 return false; // thread terminated
351 }
352 Thread.State state = snapshot.threadState();
353 StackTraceElement[] stackTrace = snapshot.stackTrace();
354
355 jsonWriter.startObject();
356 jsonWriter.writeLongProperty("tid", thread.threadId());
357 jsonWriter.writeProperty("time", now);
358 if (thread.isVirtual()) {
359 jsonWriter.writeProperty("virtual", Boolean.TRUE);
360 }
361 jsonWriter.writeProperty("name", snapshot.threadName());
362 jsonWriter.writeProperty("state", state);
363
364 // park blocker
365 Object parkBlocker = snapshot.parkBlocker();
366 if (parkBlocker != null) {
367 // parkBlocker is an object to allow for exclusiveOwnerThread in the future
368 jsonWriter.startObject("parkBlocker");
369 jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
370 if (snapshot.parkBlockerOwner() instanceof Thread owner) {
371 jsonWriter.writeLongProperty("owner", owner.threadId());
372 }
373 jsonWriter.endObject();
374 }
375
376 // blocked on monitor enter or Object.wait
377 if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
378 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
379 } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
380 && snapshot.waitingOn() instanceof Object obj) {
381 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
382 }
383
384 // stack trace
385 jsonWriter.startArray("stack");
386 Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
387 jsonWriter.endArray();
388
389 // monitors owned, skip if none
390 if (snapshot.ownsMonitors()) {
391 jsonWriter.startArray("monitorsOwned");
392 int depth = 0;
393 while (depth < stackTrace.length) {
394 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
395 if (!objs.isEmpty()) {
396 jsonWriter.startObject();
397 jsonWriter.writeProperty("depth", depth);
398 jsonWriter.startArray("locks");
399 snapshot.ownedMonitorsAt(depth)
400 .map(o -> (o != null) ? Objects.toIdentityString(o) : null)
401 .forEach(jsonWriter::writeProperty);
402 jsonWriter.endArray();
403 jsonWriter.endObject();
404 }
405 depth++;
406 }
407 jsonWriter.endArray();
408 }
409
410 // thread identifier of carrier, when mounted
411 if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
412 jsonWriter.writeLongProperty("carrier", carrier.threadId());
413 }
414
415 jsonWriter.endObject();
416 return true;
417 }
418
419 /**
420 * Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
421 * This class is not intended to be a fully featured JSON writer.
422 */
423 private static class JsonWriter {
424 private static class Node {
425 final boolean isArray;
426 int propertyCount;
427 Node(boolean isArray) {
428 this.isArray = isArray;
429 }
430 boolean isArray() {
431 return isArray;
432 }
433 int propertyCount() {
434 return propertyCount;
435 }
436 int getAndIncrementPropertyCount() {
437 int old = propertyCount;
438 propertyCount++;
439 return old;
440 }
441 }
442 private final Deque<Node> stack = new ArrayDeque<>();
443 private final boolean generateLongsAsString;
444 private final TextWriter writer;
445
446 JsonWriter(TextWriter writer, boolean generateLongsAsString) {
447 this.writer = writer;
448 this.generateLongsAsString = generateLongsAsString;
449 }
450
451 private void indent() {
452 int indent = stack.size() * 2;
453 writer.print(" ".repeat(indent));
454 }
455
456 /**
457 * Start of object or array.
458 */
459 private void startObject(String name, boolean isArray) {
460 if (!stack.isEmpty()) {
461 Node node = stack.peek();
462 if (node.getAndIncrementPropertyCount() > 0) {
463 writer.println(",");
464 }
465 }
466 indent();
467 if (name != null) {
468 writer.print("\"" + name + "\": ");
469 }
470 writer.println(isArray ? "[" : "{");
471 stack.push(new Node(isArray));
472 }
473
474 /**
475 * End of object or array.
476 */
477 private void endObject(boolean isArray) {
478 Node node = stack.pop();
479 if (node.isArray() != isArray)
480 throw new IllegalStateException();
481 if (node.propertyCount() > 0) {
482 writer.println();
483 }
484 indent();
485 writer.print(isArray ? "]" : "}");
486 }
487
488 /**
489 * Write a property.
490 * @param name the property name, null for an unnamed property
491 * @param obj the value or null
492 */
493 void writeProperty(String name, Object obj) {
494 Node node = stack.peek();
495 assert node != null;
496 if (node.getAndIncrementPropertyCount() > 0) {
497 writer.println(",");
498 }
499 indent();
500 if (name != null) {
501 writer.print("\"" + name + "\": ");
502 }
503 switch (obj) {
504 case Number _ -> writer.print(obj);
505 case Boolean _ -> writer.print(obj);
506 case null -> writer.print("null");
507 default -> writer.print("\"" + escape(obj.toString()) + "\"");
508 }
509 }
510
511 /**
512 * Write a property with a long value. If the value is outside the "interop"
513 * range of IEEE-754 double-precision floating point (64-bit) then it is
514 * written as a string.
515 */
516 void writeLongProperty(String name, long value) {
517 if (generateLongsAsString || value < -0x1FFFFFFFFFFFFFL || value > 0x1FFFFFFFFFFFFFL) {
518 writeProperty(name, Long.toString(value));
519 } else {
520 writeProperty(name, value);
521 }
522 }
523
524 /**
525 * Write an unnamed property.
526 */
527 void writeProperty(Object obj) {
528 writeProperty(null, obj);
529 }
530
531 /**
532 * Start named object.
533 */
534 void startObject(String name) {
535 startObject(name, false);
536 }
537
538 /**
539 * Start unnamed object.
540 */
541 void startObject() {
542 startObject(null);
543 }
544
545 /**
546 * End of object.
547 */
548 void endObject() {
549 endObject(false);
550 }
551
552 /**
553 * Start named array.
554 */
555 void startArray(String name) {
556 startObject(name, true);
557 }
558
559 /**
560 * End of array.
561 */
562 void endArray() {
563 endObject(true);
564 }
565
566 /**
567 * Escape any characters that need to be escape in the JSON output.
568 */
569 private static String escape(String value) {
570 StringBuilder sb = new StringBuilder();
571 for (int i = 0; i < value.length(); i++) {
572 char c = value.charAt(i);
573 switch (c) {
574 case '"' -> sb.append("\\\"");
575 case '\\' -> sb.append("\\\\");
576 case '/' -> sb.append("\\/");
577 case '\b' -> sb.append("\\b");
578 case '\f' -> sb.append("\\f");
579 case '\n' -> sb.append("\\n");
580 case '\r' -> sb.append("\\r");
581 case '\t' -> sb.append("\\t");
582 default -> {
583 if (c <= 0x1f) {
584 sb.append(String.format("\\u%04x", c));
585 } else {
586 sb.append(c);
587 }
588 }
589 }
590 }
591 return sb.toString();
592 }
593 }
594
595 /**
596 * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
597 * written the subsequent bytes are discarded.
598 */
599 private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
600 final int max;
601 BoundedByteArrayOutputStream(int max) {
602 this.max = max;
603 }
604 @Override
605 public void write(int b) {
606 if (max < count) {
607 super.write(b);
608 }
609 }
610 @Override
611 public void write(byte[] b, int off, int len) {
612 int remaining = max - count;
613 if (remaining > 0) {
614 super.write(b, off, Integer.min(len, remaining));
615 }
616 }
617 @Override
618 public void close() {
619 }
620 }
621
622 /**
623 * Simple Writer implementation for printing text. The print/println methods
624 * throw UncheckedIOException if an I/O error occurs.
625 */
626 private static class TextWriter extends Writer {
627 private final Writer delegate;
628
629 TextWriter(OutputStream out) {
630 delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
631 }
632
633 @Override
634 public void write(char[] cbuf, int off, int len) throws IOException {
635 delegate.write(cbuf, off, len);
636 }
637
638 void print(Object obj) {
639 String s = String.valueOf(obj);
640 try {
641 write(s, 0, s.length());
642 } catch (IOException ioe) {
643 throw new UncheckedIOException(ioe);
644 }
645 }
646
647 void println() {
648 print(System.lineSeparator());
649 }
650
651 void println(String s) {
652 print(s);
653 println();
654 }
655
656 void println(Object obj) {
657 print(obj);
658 println();
659 }
660
661 @Override
662 public void flush() throws IOException {
663 delegate.flush();
664 }
665
666 @Override
667 public void close() throws IOException {
668 delegate.close();
669 }
670 }
671
672 /**
673 * Returns the process ID or -1 if not supported.
674 */
675 private static long processId() {
676 try {
677 return ProcessHandle.current().pid();
678 } catch (UnsupportedOperationException e) {
679 return -1L;
680 }
681 }
682 }