1 /* 2 * Copyright (c) 2020, 2025, 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 import java.util.concurrent.locks.AbstractOwnableSynchronizer; 48 49 /** 50 * Thread dump support. 51 * 52 * This class defines static methods to support the Thread.dump_to_file diagnostic command 53 * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a 54 * thread dump to a file or byte array in plain text or JSON format. 55 */ 56 public class ThreadDumper { 57 private ThreadDumper() { } 58 59 // the maximum byte array to return when generating the thread dump to a byte array 60 private static final int MAX_BYTE_ARRAY_SIZE = 16_000; 61 62 /** 63 * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded. 64 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. 65 * 66 * @param file the file path to the file, null or "-" to return a byte array 67 * @param okayToOverwrite true to overwrite an existing file 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) { 71 if (file == null || file.equals("-")) { 72 return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE); 73 } else { 74 return dumpThreadsToFile(file, okayToOverwrite, 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) { 87 if (file == null || file.equals("-")) { 88 return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE); 89 } else { 90 return dumpThreadsToFile(file, okayToOverwrite, true); 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 and/or message to send 98 * to the tool user. 99 */ 100 private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { 101 var out = new BoundedByteArrayOutputStream(maxSize); 102 try (out; var writer = new TextWriter(out)) { 103 if (json) { 104 dumpThreadsToJson(writer); 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 and/or message to send to the tool user. 122 */ 123 private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) { 124 Path path = Path.of(file).toAbsolutePath(); 125 OpenOption[] options = (okayToOverwrite) 126 ? new OpenOption[0] 127 : new OpenOption[] { StandardOpenOption.CREATE_NEW }; 128 String reply; 129 try (OutputStream out = Files.newOutputStream(path, options)) { 130 try (var writer = new TextWriter(out)) { 131 if (json) { 132 dumpThreadsToJson(writer); 133 } else { 134 dumpThreads(writer); 135 } 136 reply = String.format("Created %s%n", path); 137 } catch (UncheckedIOException e) { 138 reply = String.format("Failed: %s%n", e.getCause()); 139 } 140 } catch (FileAlreadyExistsException _) { 141 reply = String.format("%s exists, use -overwrite to overwrite%n", path); 142 } catch (Exception ex) { 143 reply = String.format("Failed: %s%n", ex); 144 } 145 return reply.getBytes(StandardCharsets.UTF_8); 146 } 147 148 /** 149 * Generate a thread dump in plain text format to the given output stream, UTF-8 150 * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 151 * @throws IOException if an I/O error occurs 152 */ 153 public static void dumpThreads(OutputStream out) throws IOException { 154 var writer = new TextWriter(out); 155 try { 156 dumpThreads(writer); 157 writer.flush(); 158 } catch (UncheckedIOException e) { 159 IOException ioe = e.getCause(); 160 throw ioe; 161 } 162 } 163 164 /** 165 * Generate a thread dump in plain text format to the given text stream. 166 * @throws UncheckedIOException if an I/O error occurs 167 */ 168 private static void dumpThreads(TextWriter writer) { 169 writer.println(processId()); 170 writer.println(Instant.now()); 171 writer.println(Runtime.version()); 172 writer.println(); 173 dumpThreads(ThreadContainers.root(), writer); 174 } 175 176 private static void dumpThreads(ThreadContainer container, TextWriter writer) { 177 container.threads().forEach(t -> dumpThread(t, writer)); 178 container.children().forEach(c -> dumpThreads(c, writer)); 179 } 180 181 private static void dumpThread(Thread thread, TextWriter writer) { 182 ThreadSnapshot snapshot = ThreadSnapshot.of(thread); 183 Instant now = Instant.now(); 184 Thread.State state = snapshot.threadState(); 185 writer.println("#" + thread.threadId() + " \"" + snapshot.threadName() 186 + "\" " + state + " " + now); 187 188 // park blocker 189 Object parkBlocker = snapshot.parkBlocker(); 190 if (parkBlocker != null) { 191 writer.print(" // parked on " + Objects.toIdentityString(parkBlocker)); 192 if (parkBlocker instanceof AbstractOwnableSynchronizer 193 && snapshot.exclusiveOwnerThread() instanceof Thread owner) { 194 writer.print(", owned by #" + owner.threadId()); 195 } 196 writer.println(); 197 } 198 199 // blocked on monitor enter or Object.wait 200 if (state == Thread.State.BLOCKED) { 201 Object obj = snapshot.blockedOn(); 202 if (obj != null) { 203 writer.println(" // blocked on " + Objects.toIdentityString(obj)); 204 } 205 } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) { 206 Object obj = snapshot.waitingOn(); 207 if (obj != null) { 208 writer.println(" // waiting on " + Objects.toIdentityString(obj)); 209 } 210 } 211 212 StackTraceElement[] stackTrace = snapshot.stackTrace(); 213 int depth = 0; 214 while (depth < stackTrace.length) { 215 snapshot.ownedMonitorsAt(depth).forEach(obj -> { 216 writer.print(" // locked "); 217 writer.println(Objects.toIdentityString(obj)); 218 }); 219 writer.print(" "); 220 writer.println(stackTrace[depth]); 221 depth++; 222 } 223 writer.println(); 224 } 225 226 /** 227 * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded. 228 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 229 * @throws IOException if an I/O error occurs 230 */ 231 public static void dumpThreadsToJson(OutputStream out) throws IOException { 232 var writer = new TextWriter(out); 233 try { 234 dumpThreadsToJson(writer); 235 writer.flush(); 236 } catch (UncheckedIOException e) { 237 IOException ioe = e.getCause(); 238 throw ioe; 239 } 240 } 241 242 /** 243 * Generate a thread dump to the given text stream in JSON format. 244 * @throws UncheckedIOException if an I/O error occurs 245 */ 246 private static void dumpThreadsToJson(TextWriter textWriter) { 247 var jsonWriter = new JsonWriter(textWriter); 248 249 jsonWriter.startObject(); // top-level object 250 251 jsonWriter.startObject("threadDump"); 252 253 jsonWriter.writeProperty("processId", processId()); 254 jsonWriter.writeProperty("time", Instant.now()); 255 jsonWriter.writeProperty("runtimeVersion", Runtime.version()); 256 257 jsonWriter.startArray("threadContainers"); 258 dumpThreads(ThreadContainers.root(), jsonWriter); 259 jsonWriter.endArray(); 260 261 jsonWriter.endObject(); // threadDump 262 263 jsonWriter.endObject(); // end of top-level object 264 } 265 266 /** 267 * Write a thread container to the given JSON writer. 268 * @throws UncheckedIOException if an I/O error occurs 269 */ 270 private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) { 271 jsonWriter.startObject(); 272 jsonWriter.writeProperty("container", container); 273 jsonWriter.writeProperty("parent", container.parent()); 274 275 Thread owner = container.owner(); 276 jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null); 277 278 long threadCount = 0; 279 jsonWriter.startArray("threads"); 280 Iterator<Thread> threads = container.threads().iterator(); 281 while (threads.hasNext()) { 282 Thread thread = threads.next(); 283 dumpThread(thread, jsonWriter); 284 threadCount++; 285 } 286 jsonWriter.endArray(); // threads 287 288 // thread count 289 if (!ThreadContainers.trackAllThreads()) { 290 threadCount = Long.max(threadCount, container.threadCount()); 291 } 292 jsonWriter.writeProperty("threadCount", threadCount); 293 294 jsonWriter.endObject(); 295 296 // the children of the thread container follow 297 container.children().forEach(c -> dumpThreads(c, jsonWriter)); 298 } 299 300 /** 301 * Write a thread to the given JSON writer. 302 * @throws UncheckedIOException if an I/O error occurs 303 */ 304 private static void dumpThread(Thread thread, JsonWriter jsonWriter) { 305 Instant now = Instant.now(); 306 ThreadSnapshot snapshot = ThreadSnapshot.of(thread); 307 Thread.State state = snapshot.threadState(); 308 StackTraceElement[] stackTrace = snapshot.stackTrace(); 309 310 jsonWriter.startObject(); 311 jsonWriter.writeProperty("tid", thread.threadId()); 312 jsonWriter.writeProperty("time", now); 313 if (thread.isVirtual()) { 314 jsonWriter.writeProperty("virtual", Boolean.TRUE); 315 } 316 jsonWriter.writeProperty("name", snapshot.threadName()); 317 jsonWriter.writeProperty("state", state); 318 319 // park blocker 320 Object parkBlocker = snapshot.parkBlocker(); 321 if (parkBlocker != null) { 322 jsonWriter.startObject("parkBlocker"); 323 jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); 324 if (parkBlocker instanceof AbstractOwnableSynchronizer 325 && snapshot.exclusiveOwnerThread() instanceof Thread owner) { 326 jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId()); 327 } 328 jsonWriter.endObject(); 329 } 330 331 // blocked on monitor enter or Object.wait 332 if (state == Thread.State.BLOCKED) { 333 Object obj = snapshot.blockedOn(); 334 if (obj != null) { 335 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj)); 336 } 337 } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) { 338 Object obj = snapshot.waitingOn(); 339 if (obj != null) { 340 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj)); 341 } 342 } 343 344 // stack trace 345 jsonWriter.startArray("stack"); 346 Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty); 347 jsonWriter.endArray(); 348 349 // monitors owned, skip if none 350 if (snapshot.ownsMonitors()) { 351 jsonWriter.startArray("monitorsOwned"); 352 int depth = 0; 353 while (depth < stackTrace.length) { 354 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList(); 355 if (!objs.isEmpty()) { 356 jsonWriter.startObject(); 357 jsonWriter.writeProperty("depth", depth); 358 jsonWriter.startArray("locks"); 359 snapshot.ownedMonitorsAt(depth) 360 .map(Objects::toIdentityString) 361 .forEach(jsonWriter::writeProperty); 362 jsonWriter.endArray(); 363 jsonWriter.endObject(); 364 } 365 depth++; 366 } 367 jsonWriter.endArray(); 368 } 369 370 // thread identifier of carrier, when mounted 371 if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) { 372 jsonWriter.writeProperty("carrier", carrier.threadId()); 373 } 374 375 jsonWriter.endObject(); 376 } 377 378 /** 379 * Simple JSON writer to stream objects/arrays to a TextWriter with formatting. 380 * This class is not intended to be a fully featured JSON writer. 381 */ 382 private static class JsonWriter { 383 private static class Node { 384 final boolean isArray; 385 int propertyCount; 386 Node(boolean isArray) { 387 this.isArray = isArray; 388 } 389 boolean isArray() { 390 return isArray; 391 } 392 int propertyCount() { 393 return propertyCount; 394 } 395 int getAndIncrementPropertyCount() { 396 int old = propertyCount; 397 propertyCount++; 398 return old; 399 } 400 } 401 private final Deque<Node> stack = new ArrayDeque<>(); 402 private final TextWriter writer; 403 404 JsonWriter(TextWriter writer) { 405 this.writer = writer; 406 } 407 408 private void indent() { 409 int indent = stack.size() * 2; 410 writer.print(" ".repeat(indent)); 411 } 412 413 /** 414 * Start of object or array. 415 */ 416 private void startObject(String name, boolean isArray) { 417 if (!stack.isEmpty()) { 418 Node node = stack.peek(); 419 if (node.getAndIncrementPropertyCount() > 0) { 420 writer.println(","); 421 } 422 } 423 indent(); 424 if (name != null) { 425 writer.print("\"" + name + "\": "); 426 } 427 writer.println(isArray ? "[" : "{"); 428 stack.push(new Node(isArray)); 429 } 430 431 /** 432 * End of object or array. 433 */ 434 private void endObject(boolean isArray) { 435 Node node = stack.pop(); 436 if (node.isArray() != isArray) 437 throw new IllegalStateException(); 438 if (node.propertyCount() > 0) { 439 writer.println(); 440 } 441 indent(); 442 writer.print(isArray ? "]" : "}"); 443 } 444 445 /** 446 * Write a property. 447 * @param name the property name, null for an unnamed property 448 * @param obj the value or null 449 */ 450 void writeProperty(String name, Object obj) { 451 Node node = stack.peek(); 452 if (node.getAndIncrementPropertyCount() > 0) { 453 writer.println(","); 454 } 455 indent(); 456 if (name != null) { 457 writer.print("\"" + name + "\": "); 458 } 459 switch (obj) { 460 // Long may be larger than safe range of JSON integer value 461 case Long _ -> writer.print("\"" + obj + "\""); 462 case Number _ -> writer.print(obj); 463 case Boolean _ -> writer.print(obj); 464 case null -> writer.print("null"); 465 default -> writer.print("\"" + escape(obj.toString()) + "\""); 466 } 467 } 468 469 /** 470 * Write an unnamed property. 471 */ 472 void writeProperty(Object obj) { 473 writeProperty(null, obj); 474 } 475 476 /** 477 * Start named object. 478 */ 479 void startObject(String name) { 480 startObject(name, false); 481 } 482 483 /** 484 * Start unnamed object. 485 */ 486 void startObject() { 487 startObject(null); 488 } 489 490 /** 491 * End of object. 492 */ 493 void endObject() { 494 endObject(false); 495 } 496 497 /** 498 * Start named array. 499 */ 500 void startArray(String name) { 501 startObject(name, true); 502 } 503 504 /** 505 * End of array. 506 */ 507 void endArray() { 508 endObject(true); 509 } 510 511 /** 512 * Escape any characters that need to be escape in the JSON output. 513 */ 514 private static String escape(String value) { 515 StringBuilder sb = new StringBuilder(); 516 for (int i = 0; i < value.length(); i++) { 517 char c = value.charAt(i); 518 switch (c) { 519 case '"' -> sb.append("\\\""); 520 case '\\' -> sb.append("\\\\"); 521 case '/' -> sb.append("\\/"); 522 case '\b' -> sb.append("\\b"); 523 case '\f' -> sb.append("\\f"); 524 case '\n' -> sb.append("\\n"); 525 case '\r' -> sb.append("\\r"); 526 case '\t' -> sb.append("\\t"); 527 default -> { 528 if (c <= 0x1f) { 529 sb.append(String.format("\\u%04x", c)); 530 } else { 531 sb.append(c); 532 } 533 } 534 } 535 } 536 return sb.toString(); 537 } 538 } 539 540 /** 541 * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is 542 * written the subsequent bytes are discarded. 543 */ 544 private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream { 545 final int max; 546 BoundedByteArrayOutputStream(int max) { 547 this.max = max; 548 } 549 @Override 550 public void write(int b) { 551 if (max < count) { 552 super.write(b); 553 } 554 } 555 @Override 556 public void write(byte[] b, int off, int len) { 557 int remaining = max - count; 558 if (remaining > 0) { 559 super.write(b, off, Integer.min(len, remaining)); 560 } 561 } 562 @Override 563 public void close() { 564 } 565 } 566 567 /** 568 * Simple Writer implementation for printing text. The print/println methods 569 * throw UncheckedIOException if an I/O error occurs. 570 */ 571 private static class TextWriter extends Writer { 572 private final Writer delegate; 573 574 TextWriter(OutputStream out) { 575 delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 576 } 577 578 @Override 579 public void write(char[] cbuf, int off, int len) throws IOException { 580 delegate.write(cbuf, off, len); 581 } 582 583 void print(Object obj) { 584 String s = String.valueOf(obj); 585 try { 586 write(s, 0, s.length()); 587 } catch (IOException ioe) { 588 throw new UncheckedIOException(ioe); 589 } 590 } 591 592 void println() { 593 print(System.lineSeparator()); 594 } 595 596 void println(String s) { 597 print(s); 598 println(); 599 } 600 601 void println(Object obj) { 602 print(obj); 603 println(); 604 } 605 606 @Override 607 public void flush() throws IOException { 608 delegate.flush(); 609 } 610 611 @Override 612 public void close() throws IOException { 613 delegate.close(); 614 } 615 } 616 617 /** 618 * Returns the process ID or -1 if not supported. 619 */ 620 private static long processId() { 621 try { 622 return ProcessHandle.current().pid(); 623 } catch (UnsupportedOperationException e) { 624 return -1L; 625 } 626 } 627 }