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