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.Iterator; 41 import java.util.List; 42 43 /** 44 * Thread dump support. 45 * 46 * This class defines methods to dump threads to an output stream or file in plain 47 * text or JSON format. 48 */ 49 public class ThreadDumper { 50 private ThreadDumper() { } 51 52 // the maximum byte array to return when generating the thread dump to a byte array 53 private static final int MAX_BYTE_ARRAY_SIZE = 16_000; 54 55 /** 56 * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded. 57 * 58 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. 59 * 60 * @param file the file path to the file, null or "-" to return a byte array 61 * @param okayToOverwrite true to overwrite an existing file 62 * @return the UTF-8 encoded thread dump or message to return to the user 63 */ 64 public static byte[] dumpThreads(String file, boolean okayToOverwrite) { 65 if (file == null || file.equals("-")) { 66 return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE); 67 } else { 68 return dumpThreadsToFile(file, okayToOverwrite, false); 69 } 70 } 71 72 /** 73 * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded. 74 * 75 * This method is invoked by the VM for the Thread.dump_to_file diagnostic command. 76 * 77 * @param file the file path to the file, null or "-" to return a byte array 78 * @param okayToOverwrite true to overwrite an existing file 79 * @return the UTF-8 encoded thread dump or message to return to the user 80 */ 81 public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) { 82 if (file == null || file.equals("-")) { 83 return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE); 84 } else { 85 return dumpThreadsToFile(file, okayToOverwrite, true); 86 } 87 } 88 89 /** 90 * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded. 91 */ 92 private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) { 93 try (var out = new BoundedByteArrayOutputStream(maxSize); 94 PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) { 95 if (json) { 96 dumpThreadsToJson(ps); 97 } else { 98 dumpThreads(ps); 99 } 100 return out.toByteArray(); 101 } 102 } 103 104 /** 105 * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded. 106 */ 107 private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) { 108 Path path = Path.of(file).toAbsolutePath(); 109 OpenOption[] options = (okayToOverwrite) 110 ? new OpenOption[0] 111 : new OpenOption[] { StandardOpenOption.CREATE_NEW }; 112 String reply; 113 try (OutputStream out = Files.newOutputStream(path, options); 114 BufferedOutputStream bos = new BufferedOutputStream(out); 115 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) { 116 if (json) { 117 dumpThreadsToJson(ps); 118 } else { 119 dumpThreads(ps); 120 } 121 reply = String.format("Created %s%n", path); 122 } catch (FileAlreadyExistsException e) { 123 reply = String.format("%s exists, use -overwrite to overwrite%n", path); 124 } catch (IOException ioe) { 125 reply = String.format("Failed: %s%n", ioe); 126 } 127 return reply.getBytes(StandardCharsets.UTF_8); 128 } 129 130 /** 131 * Generate a thread dump in plain text format to the given output stream, 132 * UTF-8 encoded. 133 * 134 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 135 */ 136 public static void dumpThreads(OutputStream out) { 137 BufferedOutputStream bos = new BufferedOutputStream(out); 138 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); 139 try { 140 dumpThreads(ps); 141 } finally { 142 ps.flush(); // flushes underlying stream 143 } 144 } 145 146 /** 147 * Generate a thread dump in plain text format to the given print stream. 148 */ 149 private static void dumpThreads(PrintStream ps) { 150 ps.println(processId()); 151 ps.println(Instant.now()); 152 ps.println(Runtime.version()); 153 ps.println(); 154 dumpThreads(ThreadContainers.root(), ps); 155 } 156 157 private static void dumpThreads(ThreadContainer container, PrintStream ps) { 158 container.threads().forEach(t -> dumpThread(t, ps)); 159 container.children().forEach(c -> dumpThreads(c, ps)); 160 } 161 162 private static void dumpThread(Thread thread, PrintStream ps) { 163 String suffix = thread.isVirtual() ? " virtual" : ""; 164 ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix); 165 for (StackTraceElement ste : thread.getStackTrace()) { 166 ps.print(" "); 167 ps.println(ste); 168 } 169 ps.println(); 170 } 171 172 /** 173 * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded. 174 * 175 * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads. 176 */ 177 public static void dumpThreadsToJson(OutputStream out) { 178 BufferedOutputStream bos = new BufferedOutputStream(out); 179 PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8); 180 try { 181 dumpThreadsToJson(ps); 182 } finally { 183 ps.flush(); // flushes underlying stream 184 } 185 } 186 187 /** 188 * Generate a thread dump to the given print stream in JSON format. 189 */ 190 private static void dumpThreadsToJson(PrintStream out) { 191 out.println("{"); 192 out.println(" \"threadDump\": {"); 193 194 String now = Instant.now().toString(); 195 String runtimeVersion = Runtime.version().toString(); 196 out.format(" \"processId\": \"%d\",%n", processId()); 197 out.format(" \"time\": \"%s\",%n", escape(now)); 198 out.format(" \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion)); 199 200 out.println(" \"threadContainers\": ["); 201 List<ThreadContainer> containers = allContainers(); 202 Iterator<ThreadContainer> iterator = containers.iterator(); 203 while (iterator.hasNext()) { 204 ThreadContainer container = iterator.next(); 205 boolean more = iterator.hasNext(); 206 dumpThreadsToJson(container, out, more); 207 } 208 out.println(" ]"); // end of threadContainers 209 210 out.println(" }"); // end threadDump 211 out.println("}"); // end object 212 } 213 214 /** 215 * Dump the given thread container to the print stream in JSON format. 216 */ 217 private static void dumpThreadsToJson(ThreadContainer container, 218 PrintStream out, 219 boolean more) { 220 out.println(" {"); 221 out.format(" \"container\": \"%s\",%n", escape(container.toString())); 222 223 ThreadContainer parent = container.parent(); 224 if (parent == null) { 225 out.format(" \"parent\": null,%n"); 226 } else { 227 out.format(" \"parent\": \"%s\",%n", escape(parent.toString())); 228 } 229 230 Thread owner = container.owner(); 231 if (owner == null) { 232 out.format(" \"owner\": null,%n"); 233 } else { 234 out.format(" \"owner\": \"%d\",%n", owner.threadId()); 235 } 236 237 long threadCount = 0; 238 out.println(" \"threads\": ["); 239 Iterator<Thread> threads = container.threads().iterator(); 240 while (threads.hasNext()) { 241 Thread thread = threads.next(); 242 dumpThreadToJson(thread, out, threads.hasNext()); 243 threadCount++; 244 } 245 out.println(" ],"); // end of threads 246 247 // thread count 248 if (!ThreadContainers.trackAllThreads()) { 249 threadCount = Long.max(threadCount, container.threadCount()); 250 } 251 out.format(" \"threadCount\": \"%d\"%n", threadCount); 252 253 if (more) { 254 out.println(" },"); 255 } else { 256 out.println(" }"); // last container, no trailing comma 257 } 258 } 259 260 /** 261 * Dump the given thread and its stack trace to the print stream in JSON format. 262 */ 263 private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) { 264 out.println(" {"); 265 out.println(" \"tid\": \"" + thread.threadId() + "\","); 266 out.println(" \"name\": \"" + escape(thread.getName()) + "\","); 267 out.println(" \"stack\": ["); 268 269 int i = 0; 270 StackTraceElement[] stackTrace = thread.getStackTrace(); 271 while (i < stackTrace.length) { 272 out.print(" \""); 273 out.print(escape(stackTrace[i].toString())); 274 out.print("\""); 275 i++; 276 if (i < stackTrace.length) { 277 out.println(","); 278 } else { 279 out.println(); // last element, no trailing comma 280 } 281 } 282 out.println(" ]"); 283 if (more) { 284 out.println(" },"); 285 } else { 286 out.println(" }"); // last thread, no trailing comma 287 } 288 } 289 290 /** 291 * Returns a list of all thread containers that are "reachable" from 292 * the root container. 293 */ 294 private static List<ThreadContainer> allContainers() { 295 List<ThreadContainer> containers = new ArrayList<>(); 296 collect(ThreadContainers.root(), containers); 297 return containers; 298 } 299 300 private static void collect(ThreadContainer container, List<ThreadContainer> containers) { 301 containers.add(container); 302 container.children().forEach(c -> collect(c, containers)); 303 } 304 305 /** 306 * Escape any characters that need to be escape in the JSON output. 307 */ 308 private static String escape(String value) { 309 StringBuilder sb = new StringBuilder(); 310 for (int i = 0; i < value.length(); i++) { 311 char c = value.charAt(i); 312 switch (c) { 313 case '"' -> sb.append("\\\""); 314 case '\\' -> sb.append("\\\\"); 315 case '/' -> sb.append("\\/"); 316 case '\b' -> sb.append("\\b"); 317 case '\f' -> sb.append("\\f"); 318 case '\n' -> sb.append("\\n"); 319 case '\r' -> sb.append("\\r"); 320 case '\t' -> sb.append("\\t"); 321 default -> { 322 if (c <= 0x1f) { 323 sb.append(String.format("\\u%04x", c)); 324 } else { 325 sb.append(c); 326 } 327 } 328 } 329 } 330 return sb.toString(); 331 } 332 333 /** 334 * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is 335 * written the subsequent bytes are discarded. 336 */ 337 private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream { 338 final int max; 339 BoundedByteArrayOutputStream(int max) { 340 this.max = max; 341 } 342 @Override 343 public void write(int b) { 344 if (max < count) { 345 super.write(b); 346 } 347 } 348 @Override 349 public void write(byte[] b, int off, int len) { 350 int remaining = max - count; 351 if (remaining > 0) { 352 super.write(b, off, Integer.min(len, remaining)); 353 } 354 } 355 @Override 356 public void close() { 357 } 358 } 359 360 /** 361 * Returns the process ID or -1 if not supported. 362 */ 363 private static long processId() { 364 try { 365 return ProcessHandle.current().pid(); 366 } catch (UnsupportedOperationException e) { 367 return -1L; 368 } 369 } 370 }