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 }