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 ps) { 222 try (JsonWriter jsonWriter = JsonWriter.wrap(ps)) { 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 Instant now = Instant.now(); 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.startObject("parkBlocker"); 286 jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); 287 // TBD add exclusiveOwnerThread if AbstractOwnableSynchronizer 288 jsonWriter.endObject(); 289 } 290 291 // blocked on monitor enter or Object.wait 292 if (state == Thread.State.BLOCKED) { 293 Object obj = snapshot.blockedOn(); 294 if (obj != null) { 295 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj)); 296 } 297 } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) { 298 Object obj = snapshot.waitingOn(); 299 if (obj != null) { 300 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj)); 301 } 302 } 303 304 // stack trace 305 jsonWriter.startArray("stack"); 306 Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty); 307 jsonWriter.endArray(); 308 309 // monitors owned, skip if none 310 if (snapshot.ownsMonitors()) { 311 jsonWriter.startArray("monitorsOwned"); 312 int depth = 0; 313 while (depth < stackTrace.length) { 314 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList(); 315 if (!objs.isEmpty()) { 316 jsonWriter.startObject(); 317 jsonWriter.writeProperty("depth", depth); 318 jsonWriter.startArray("locks"); 319 snapshot.ownedMonitorsAt(depth) 320 .map(Objects::toIdentityString) 321 .forEach(jsonWriter::writeProperty); 322 jsonWriter.endArray(); 323 jsonWriter.endObject(); 324 } 325 depth++; 326 } 327 jsonWriter.endArray(); 328 } 329 330 jsonWriter.endObject(); 331 } 332 333 /** 334 * Returns a list of all thread containers that are "reachable" from 335 * the root container. 336 */ 337 private static List<ThreadContainer> allContainers() { 338 List<ThreadContainer> containers = new ArrayList<>(); 339 collect(ThreadContainers.root(), containers); 340 return containers; 341 } 342 343 private static void collect(ThreadContainer container, List<ThreadContainer> containers) { 344 containers.add(container); 345 container.children().forEach(c -> collect(c, containers)); 346 } 347 348 /** 349 * Simple JSON writer to stream objects/arrays to a PrintStream. 350 */ 351 private static class JsonWriter implements AutoCloseable { 352 private final PrintStream out; 353 354 // current depth and indentation 355 private int depth = -1; 356 private int indent; 357 358 // indicates if there are properties at depth N 359 private boolean[] hasProperties = new boolean[10]; 360 361 private JsonWriter(PrintStream out) { 362 this.out = out; 363 } 364 365 static JsonWriter wrap(PrintStream out) { 366 var writer = new JsonWriter(out); 367 writer.startObject(); 368 return writer; 369 } 370 371 /** 372 * Start of object or array. 373 */ 374 private void startObject(String name, boolean array) { 375 if (depth >= 0) { 376 if (hasProperties[depth]) { 377 out.println(","); 378 } else { 379 hasProperties[depth] = true; // first property at this depth 380 } 381 } 382 out.print(" ".repeat(indent)); 383 if (name != null) { 384 out.print("\"" + name + "\": "); 385 } 386 if (array) { 387 out.println("["); 388 } else { 389 out.println("{"); 390 } 391 indent += 2; 392 depth++; 393 hasProperties[depth] = false; 394 } 395 396 /** 397 * End of object or array. 398 */ 399 private void endObject(boolean array) { 400 if (hasProperties[depth]) { 401 out.println(); 402 hasProperties[depth] = false; 403 } 404 depth--; 405 indent -= 2; 406 out.print(" ".repeat(indent)); 407 if (array) { 408 out.print("]"); 409 } else { 410 out.print("}"); 411 } 412 } 413 414 /** 415 * Write a property. 416 * @param name the property name, null for an unnamed property 417 * @param obj the value or null 418 */ 419 void writeProperty(String name, Object obj) { 420 if (hasProperties[depth]) { 421 out.println(","); 422 } else { 423 hasProperties[depth] = true; 424 } 425 out.print(" ".repeat(indent)); 426 if (name != null) { 427 out.print("\"" + name + "\": "); 428 } 429 switch (obj) { 430 case Number _ -> out.print(obj); 431 case null -> out.print("null"); 432 default -> out.print("\"" + escape(obj.toString()) + "\""); 433 } 434 } 435 436 /** 437 * Write an unnamed property. 438 */ 439 void writeProperty(Object obj) { 440 writeProperty(null, obj); 441 } 442 443 /** 444 * Start named object. 445 */ 446 void startObject(String name) { 447 startObject(name, false); 448 } 449 450 /** 451 * Start unnamed object. 452 */ 453 void startObject() { 454 startObject(null); 455 } 456 457 /** 458 * End of object. 459 */ 460 void endObject() { 461 endObject(false); 462 } 463 464 /** 465 * Start named array. 466 */ 467 void startArray(String name) { 468 startObject(name, true); 469 } 470 471 /** 472 * End of array. 473 */ 474 void endArray() { 475 endObject(true); 476 } 477 478 @Override 479 public void close() { 480 endObject(); 481 out.flush(); 482 } 483 484 /** 485 * Escape any characters that need to be escape in the JSON output. 486 */ 487 private static String escape(String value) { 488 StringBuilder sb = new StringBuilder(); 489 for (int i = 0; i < value.length(); i++) { 490 char c = value.charAt(i); 491 switch (c) { 492 case '"' -> sb.append("\\\""); 493 case '\\' -> sb.append("\\\\"); 494 case '/' -> sb.append("\\/"); 495 case '\b' -> sb.append("\\b"); 496 case '\f' -> sb.append("\\f"); 497 case '\n' -> sb.append("\\n"); 498 case '\r' -> sb.append("\\r"); 499 case '\t' -> sb.append("\\t"); 500 default -> { 501 if (c <= 0x1f) { 502 sb.append(String.format("\\u%04x", c)); 503 } else { 504 sb.append(c); 505 } 506 } 507 } 508 } 509 return sb.toString(); 510 } 511 } 512 513 /** 514 * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is 515 * written the subsequent bytes are discarded. 516 */ 517 private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream { 518 final int max; 519 BoundedByteArrayOutputStream(int max) { 520 this.max = max; 521 } 522 @Override 523 public void write(int b) { 524 if (max < count) { 525 super.write(b); 526 } 527 } 528 @Override 529 public void write(byte[] b, int off, int len) { 530 int remaining = max - count; 531 if (remaining > 0) { 532 super.write(b, off, Integer.min(len, remaining)); 533 } 534 } 535 @Override 536 public void close() { 537 } 538 } 539 540 /** 541 * Returns the process ID or -1 if not supported. 542 */ 543 private static long processId() { 544 try { 545 return ProcessHandle.current().pid(); 546 } catch (UnsupportedOperationException e) { 547 return -1L; 548 } 549 } 550 }