1 /* 2 * Copyright (c) 2020, 2023, 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.BufferedOutputStream; 28 import java.io.ByteArrayOutputStream; 29 import java.io.IOException; 30 import java.io.OutputStream; 31 import java.io.PrintStream; 32 import java.nio.charset.StandardCharsets; 33 import java.nio.file.FileAlreadyExistsException; 34 import java.nio.file.Files; 35 import java.nio.file.OpenOption; 36 import java.nio.file.Path; 37 import java.nio.file.StandardOpenOption; 38 import java.time.Instant; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Iterator; 42 import java.util.List; 43 import java.util.Objects; 44 45 /** 46 * Thread dump support. 47 * 48 * This class defines methods to dump threads to an output stream or file in plain 49 * text or JSON format. 50 */ 51 public class ThreadDumper { 52 private ThreadDumper() { } 53 54 // the maximum byte array to return when generating the thread dump to a byte array 55 private static final int MAX_BYTE_ARRAY_SIZE = 16_000; 56 57 /** 58 * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded. 59 * 60 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. 61 * 62 * @param file the file path to the file, null or "-" to return a byte array 63 * @param okayToOverwrite true to overwrite an existing file 64 * @return the UTF-8 encoded thread dump or message to return to the user 65 */ 66 public static byte[] dumpThreads(String file, boolean okayToOverwrite) { 67 if (file == null || file.equals("-")) { 68 return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE); 69 } else { 70 return dumpThreadsToFile(file, okayToOverwrite, false); 71 } 72 } 73 74 /** 75 * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded. 76 * 77 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. 78 * 79 * @param file the file path to the file, null or "-" to return a byte array 80 * @param okayToOverwrite true to overwrite an existing file 81 * @return the UTF-8 encoded thread dump or message to return to the user 82 */ 83 public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) { 84 if (file == null || file.equals("-")) { 85 return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE); 86 } else { 87 return dumpThreadsToFile(file, okayToOverwrite, true); 88 } 89 } 90 91 /** 92 * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded. 93 */ 94 private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { 95 try (var out = new BoundedByteArrayOutputStream(maxSize); 96 PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) { 97 if (json) { 98 dumpThreadsToJson(ps); 99 } else { 100 dumpThreads(ps); 101 } 102 return out.toByteArray(); 103 } 104 } 105 106 /** 107 * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded. 108 */ 109 private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) { 110 Path path = Path.of(file).toAbsolutePath(); 111 OpenOption[] options = (okayToOverwrite) 112 ? new OpenOption[0] 113 : new OpenOption[] { StandardOpenOption.CREATE_NEW }; 114 String reply; 115 try (OutputStream out = Files.newOutputStream(path, options); 116 BufferedOutputStream bos = new BufferedOutputStream(out); 117 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) { 118 if (json) { 119 dumpThreadsToJson(ps); 120 } else { 121 dumpThreads(ps); 122 } 123 reply = String.format("Created %s%n", path); 124 } catch (FileAlreadyExistsException e) { 125 reply = String.format("%s exists, use -overwrite to overwrite%n", path); 126 } catch (IOException ioe) { 127 reply = String.format("Failed: %s%n", ioe); 128 } 129 return reply.getBytes(StandardCharsets.UTF_8); 130 } 131 132 /** 133 * Generate a thread dump in plain text format to the given output stream, 134 * UTF-8 encoded. 135 * 136 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 137 */ 138 public static void dumpThreads(OutputStream out) { 139 BufferedOutputStream bos = new BufferedOutputStream(out); 140 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); 141 try { 142 dumpThreads(ps); 143 } finally { 144 ps.flush(); // flushes underlying stream 145 } 146 } 147 148 /** 149 * Generate a thread dump in plain text format to the given print stream. 150 */ 151 private static void dumpThreads(PrintStream ps) { 152 ps.println(processId()); 153 ps.println(Instant.now()); 154 ps.println(Runtime.version()); 155 ps.println(); 156 dumpThreads(ThreadContainers.root(), ps); 157 } 158 159 private static void dumpThreads(ThreadContainer container, PrintStream ps) { 160 container.threads().forEach(t -> dumpThread(t, ps)); 161 container.children().forEach(c -> dumpThreads(c, ps)); 162 } 163 164 private static void dumpThread(Thread thread, PrintStream ps) { 165 ThreadSnapshot snapshot = ThreadSnapshot.of(thread); 166 Thread.State state = snapshot.threadState(); 167 ps.println("#" + thread.threadId() + " \"" + snapshot.threadName() 168 + "\" " + state + " " + Instant.now()); 169 170 // park blocker 171 Object parkBlocker = snapshot.parkBlocker(); 172 if (parkBlocker != null) { 173 ps.println(" // parked on " + Objects.toIdentityString(parkBlocker)); 174 } 175 176 // blocked on monitor enter or Object.wait 177 if (state == Thread.State.BLOCKED) { 178 Object obj = snapshot.blockedOn(); 179 if (obj != null) { 180 ps.println(" // blocked on " + Objects.toIdentityString(obj)); 181 } 182 } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) { 183 Object obj = snapshot.waitingOn(); 184 if (obj != null) { 185 ps.println(" // waiting on " + Objects.toIdentityString(obj)); 186 } 187 } 188 189 StackTraceElement[] stackTrace = snapshot.stackTrace(); 190 int depth = 0; 191 while (depth < stackTrace.length) { 192 snapshot.ownedMonitorsAt(depth).forEach(obj -> { 193 ps.print(" // locked "); 194 ps.println(Objects.toIdentityString(obj)); 195 }); 196 ps.print(" "); 197 ps.println(stackTrace[depth]); 198 depth++; 199 } 200 ps.println(); 201 } 202 203 /** 204 * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded. 205 * 206 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 207 */ 208 public static void dumpThreadsToJson(OutputStream out) { 209 BufferedOutputStream bos = new BufferedOutputStream(out); 210 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); 211 try { 212 dumpThreadsToJson(ps); 213 } finally { 214 ps.flush(); // flushes underlying stream 215 } 216 } 217 218 /** 219 * Generate a thread dump to the given print stream in JSON format. 220 */ 221 private static void dumpThreadsToJson(PrintStream out) { 222 try (JsonWriter jsonWriter = JsonWriter.wrap(out)) { 223 jsonWriter.startObject("threadDump"); 224 225 jsonWriter.writeProperty("processId", processId()); 226 jsonWriter.writeProperty("time", Instant.now()); 227 jsonWriter.writeProperty("runtimeVersion", Runtime.version()); 228 229 jsonWriter.startArray("threadContainers"); 230 allContainers().forEach(c -> dumpThreadsToJson(c, jsonWriter)); 231 jsonWriter.endArray(); 232 233 jsonWriter.endObject(); // threadDump 234 } 235 } 236 237 /** 238 * Write a thread container to the given JSON writer. 239 */ 240 private static void dumpThreadsToJson(ThreadContainer container, JsonWriter jsonWriter) { 241 jsonWriter.startObject(); 242 jsonWriter.writeProperty("container", container); 243 jsonWriter.writeProperty("parent", container.parent()); 244 245 Thread owner = container.owner(); 246 jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null); 247 248 long threadCount = 0; 249 jsonWriter.startArray("threads"); 250 Iterator<Thread> threads = container.threads().iterator(); 251 while (threads.hasNext()) { 252 Thread thread = threads.next(); 253 dumpThreadToJson(thread, jsonWriter); 254 threadCount++; 255 } 256 jsonWriter.endArray(); // threads 257 258 // thread count 259 if (!ThreadContainers.trackAllThreads()) { 260 threadCount = Long.max(threadCount, container.threadCount()); 261 } 262 jsonWriter.writeProperty("threadCount", threadCount); 263 264 jsonWriter.endObject(); 265 } 266 267 /** 268 * Write a thread to the given JSON writer. 269 */ 270 private static void dumpThreadToJson(Thread thread, JsonWriter jsonWriter) { 271 String now = Instant.now().toString(); 272 ThreadSnapshot snapshot = ThreadSnapshot.of(thread); 273 Thread.State state = snapshot.threadState(); 274 StackTraceElement[] stackTrace = snapshot.stackTrace(); 275 276 jsonWriter.startObject(); 277 jsonWriter.writeProperty("tid", thread.threadId()); 278 jsonWriter.writeProperty("time", now); 279 jsonWriter.writeProperty("name", snapshot.threadName()); 280 jsonWriter.writeProperty("state", state); 281 282 // park blocker 283 Object parkBlocker = snapshot.parkBlocker(); 284 if (parkBlocker != null) { 285 jsonWriter.writeProperty("parkBlocker", Objects.toIdentityString(parkBlocker)); 286 } 287 288 // blocked on monitor enter or Object.wait 289 if (state == Thread.State.BLOCKED) { 290 Object obj = snapshot.blockedOn(); 291 if (obj != null) { 292 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj)); 293 } 294 } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) { 295 Object obj = snapshot.waitingOn(); 296 if (obj != null) { 297 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj)); 298 } 299 } 300 301 // stack trace 302 jsonWriter.startArray("stack"); 303 Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty); 304 jsonWriter.endArray(); 305 306 // monitors owned, skip if none 307 if (snapshot.ownsMonitors()) { 308 jsonWriter.startArray("monitorsOwned"); 309 int depth = 0; 310 while (depth < stackTrace.length) { 311 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList(); 312 if (!objs.isEmpty()) { 313 jsonWriter.startObject(); 314 jsonWriter.writeProperty("depth", depth); 315 jsonWriter.startArray("locks"); 316 snapshot.ownedMonitorsAt(depth) 317 .map(Objects::toIdentityString) 318 .forEach(jsonWriter::writeProperty); 319 jsonWriter.endArray(); 320 jsonWriter.endObject(); 321 } 322 depth++; 323 } 324 jsonWriter.endArray(); 325 } 326 327 jsonWriter.endObject(); 328 } 329 330 /** 331 * Returns a list of all thread containers that are "reachable" from 332 * the root container. 333 */ 334 private static List<ThreadContainer> allContainers() { 335 List<ThreadContainer> containers = new ArrayList<>(); 336 collect(ThreadContainers.root(), containers); 337 return containers; 338 } 339 340 private static void collect(ThreadContainer container, List<ThreadContainer> containers) { 341 containers.add(container); 342 container.children().forEach(c -> collect(c, containers)); 343 } 344 345 /** 346 * Simple JSON writer to stream objects/arrays to a PrintStream. 347 */ 348 private static class JsonWriter implements AutoCloseable { 349 private final PrintStream out; 350 351 // current depth and indentation 352 private int depth = -1; 353 private int indent; 354 355 // indicates if there are properties at depth N 356 private boolean[] hasProperties = new boolean[10]; 357 358 private JsonWriter(PrintStream out) { 359 this.out = out; 360 } 361 362 static JsonWriter wrap(PrintStream out) { 363 var writer = new JsonWriter(out); 364 writer.startObject(); 365 return writer; 366 } 367 368 /** 369 * Start of object or array. 370 */ 371 private void startObject(String name, boolean array) { 372 if (depth >= 0) { 373 if (hasProperties[depth]) { 374 out.println(","); 375 } else { 376 hasProperties[depth] = true; // first property at this depth 377 } 378 } 379 out.print(" ".repeat(indent)); 380 if (name != null) { 381 out.print("\"" + name + "\": "); 382 } 383 if (array) { 384 out.println("["); 385 } else { 386 out.println("{"); 387 } 388 indent += 2; 389 depth++; 390 hasProperties[depth] = false; 391 } 392 393 /** 394 * End of object or array. 395 */ 396 private void endObject(boolean array) { 397 if (hasProperties[depth]) { 398 out.println(); 399 hasProperties[depth] = false; 400 } 401 depth--; 402 indent -= 2; 403 out.print(" ".repeat(indent)); 404 if (array) { 405 out.print("]"); 406 } else { 407 out.print("}"); 408 } 409 } 410 411 /** 412 * Write a named property. 413 */ 414 void writeProperty(String name, Object obj) { 415 if (hasProperties[depth]) { 416 out.println(","); 417 } else { 418 hasProperties[depth] = true; 419 } 420 out.print(" ".repeat(indent)); 421 if (name != null) { 422 out.print("\"" + name + "\": "); 423 } 424 if (obj != null) { 425 out.print("\"" + escape(obj.toString()) + "\""); 426 } else { 427 out.print("null"); 428 } 429 } 430 431 /** 432 * Write an unnamed property. 433 */ 434 void writeProperty(Object obj) { 435 writeProperty(null, obj); 436 } 437 438 /** 439 * Start named object. 440 */ 441 void startObject(String name) { 442 startObject(name, false); 443 } 444 445 /** 446 * Start unnamed object. 447 */ 448 void startObject() { 449 startObject(null); 450 } 451 452 /** 453 * End of object. 454 */ 455 void endObject() { 456 endObject(false); 457 } 458 459 /** 460 * Start named array. 461 */ 462 void startArray(String name) { 463 startObject(name, true); 464 } 465 466 /** 467 * End of array. 468 */ 469 void endArray() { 470 endObject(true); 471 } 472 473 @Override 474 public void close() { 475 endObject(); 476 out.flush(); 477 } 478 479 /** 480 * Escape any characters that need to be escape in the JSON output. 481 */ 482 private static String escape(String value) { 483 StringBuilder sb = new StringBuilder(); 484 for (int i = 0; i < value.length(); i++) { 485 char c = value.charAt(i); 486 switch (c) { 487 case '"' -> sb.append("\\\""); 488 case '\\' -> sb.append("\\\\"); 489 case '/' -> sb.append("\\/"); 490 case '\b' -> sb.append("\\b"); 491 case '\f' -> sb.append("\\f"); 492 case '\n' -> sb.append("\\n"); 493 case '\r' -> sb.append("\\r"); 494 case '\t' -> sb.append("\\t"); 495 default -> { 496 if (c <= 0x1f) { 497 sb.append(String.format("\\u%04x", c)); 498 } else { 499 sb.append(c); 500 } 501 } 502 } 503 } 504 return sb.toString(); 505 } 506 } 507 508 /** 509 * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is 510 * written the subsequent bytes are discarded. 511 */ 512 private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream { 513 final int max; 514 BoundedByteArrayOutputStream(int max) { 515 this.max = max; 516 } 517 @Override 518 public void write(int b) { 519 if (max < count) { 520 super.write(b); 521 } 522 } 523 @Override 524 public void write(byte[] b, int off, int len) { 525 int remaining = max - count; 526 if (remaining > 0) { 527 super.write(b, off, Integer.min(len, remaining)); 528 } 529 } 530 @Override 531 public void close() { 532 } 533 } 534 535 /** 536 * Returns the process ID or -1 if not supported. 537 */ 538 private static long processId() { 539 try { 540 return ProcessHandle.current().pid(); 541 } catch (UnsupportedOperationException e) { 542 return -1L; 543 } 544 } 545 }