< prev index next >

src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java

Print this page
@@ -1,7 +1,7 @@
  /*
-  * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
+  * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
   * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   *
   * This code is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 only, as
   * published by the Free Software Foundation.  Oracle designates this

@@ -22,63 +22,68 @@
   * or visit www.oracle.com if you need additional information or have any
   * questions.
   */
  package jdk.internal.vm;
  
- import java.io.BufferedOutputStream;
+ import java.io.BufferedWriter;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.OutputStream;
- import java.io.PrintStream;
+ import java.io.OutputStreamWriter;
+ import java.io.UncheckedIOException;
+ import java.io.Writer;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.FileAlreadyExistsException;
  import java.nio.file.Files;
  import java.nio.file.OpenOption;
  import java.nio.file.Path;
  import java.nio.file.StandardOpenOption;
  import java.time.Instant;
- import java.util.ArrayList;
+ import java.util.ArrayDeque;
+ import java.util.Arrays;
+ import java.util.Deque;
  import java.util.Iterator;
  import java.util.List;
+ import java.util.Objects;
+ import java.util.concurrent.locks.AbstractOwnableSynchronizer;
  
  /**
   * Thread dump support.
   *
-  * This class defines methods to dump threads to an output stream or file in plain
-  * text or JSON format.
+  * This class defines static methods to support the Thread.dump_to_file diagnostic command
+  * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
+  * thread dump to a file or byte array in plain text or JSON format.
   */
  public class ThreadDumper {
      private ThreadDumper() { }
  
      // the maximum byte array to return when generating the thread dump to a byte array
      private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
  
      /**
-      * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded.
-      *
+      * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.
       * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
       *
       * @param file the file path to the file, null or "-" to return a byte array
       * @param okayToOverwrite true to overwrite an existing file
-      * @return the UTF-8 encoded thread dump or message to return to the user
+      * @return the UTF-8 encoded thread dump or message to return to the tool user
       */
      public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
          if (file == null || file.equals("-")) {
              return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
          } else {
              return dumpThreadsToFile(file, okayToOverwrite, false);
          }
      }
  
      /**
-      * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded.
-      *
+      * Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded.
       * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
       *
       * @param file the file path to the file, null or "-" to return a byte array
       * @param okayToOverwrite true to overwrite an existing file
-      * @return the UTF-8 encoded thread dump or message to return to the user
+      * @return the UTF-8 encoded thread dump or message to return to the tool user
       */
      public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
          if (file == null || file.equals("-")) {
              return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
          } else {

@@ -86,250 +91,452 @@
          }
      }
  
      /**
       * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
+      * This method is the implementation of the Thread.dump_to_file diagnostic command
+      * when a file path is not specified. It returns the thread and/or message to send
+      * to the tool user.
       */
      private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
-         try (var out = new BoundedByteArrayOutputStream(maxSize);
-              PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
+         var out = new BoundedByteArrayOutputStream(maxSize);
+         try (out; var writer = new TextWriter(out)) {
              if (json) {
-                 dumpThreadsToJson(ps);
+                 dumpThreadsToJson(writer);
              } else {
-                 dumpThreads(ps);
+                 dumpThreads(writer);
              }
-             return out.toByteArray();
+         } catch (Exception ex) {
+             if (ex instanceof UncheckedIOException ioe) {
+                 ex = ioe.getCause();
+             }
+             String reply = String.format("Failed: %s%n", ex);
+             return reply.getBytes(StandardCharsets.UTF_8);
          }
+         return out.toByteArray();
      }
  
      /**
       * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
+      * This method is the implementation of the Thread.dump_to_file diagnostic command.
+      * It returns the thread and/or message to send to the tool user.
       */
      private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
          Path path = Path.of(file).toAbsolutePath();
          OpenOption[] options = (okayToOverwrite)
                  ? new OpenOption[0]
                  : new OpenOption[] { StandardOpenOption.CREATE_NEW };
          String reply;
-         try (OutputStream out = Files.newOutputStream(path, options);
-              BufferedOutputStream bos = new BufferedOutputStream(out);
-              PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) {
-             if (json) {
-                 dumpThreadsToJson(ps);
-             } else {
-                 dumpThreads(ps);
+         try (OutputStream out = Files.newOutputStream(path, options)) {
+             try (var writer = new TextWriter(out)) {
+                 if (json) {
+                     dumpThreadsToJson(writer);
+                 } else {
+                     dumpThreads(writer);
+                 }
+                 reply = String.format("Created %s%n", path);
+             } catch (UncheckedIOException e) {
+                 reply = String.format("Failed: %s%n", e.getCause());
              }
-             reply = String.format("Created %s%n", path);
-         } catch (FileAlreadyExistsException e) {
+         } catch (FileAlreadyExistsException _) {
              reply = String.format("%s exists, use -overwrite to overwrite%n", path);
-         } catch (IOException ioe) {
-             reply = String.format("Failed: %s%n", ioe);
+         } catch (Exception ex) {
+             reply = String.format("Failed: %s%n", ex);
          }
          return reply.getBytes(StandardCharsets.UTF_8);
      }
  
      /**
-      * Generate a thread dump in plain text format to the given output stream,
-      * UTF-8 encoded.
-      *
-      * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
+      * Generate a thread dump in plain text format to the given output stream, UTF-8
+      * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
+      * @throws IOException if an I/O error occurs
       */
-     public static void dumpThreads(OutputStream out) {
-         BufferedOutputStream bos = new BufferedOutputStream(out);
-         PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
+     public static void dumpThreads(OutputStream out) throws IOException {
+         var writer = new TextWriter(out);
          try {
-             dumpThreads(ps);
-         } finally {
-             ps.flush();  // flushes underlying stream
+             dumpThreads(writer);
+             writer.flush();
+         } catch (UncheckedIOException e) {
+             IOException ioe = e.getCause();
+             throw ioe;
          }
      }
  
      /**
-      * Generate a thread dump in plain text format to the given print stream.
+      * Generate a thread dump in plain text format to the given text stream.
+      * @throws UncheckedIOException if an I/O error occurs
       */
-     private static void dumpThreads(PrintStream ps) {
-         ps.println(processId());
-         ps.println(Instant.now());
-         ps.println(Runtime.version());
-         ps.println();
-         dumpThreads(ThreadContainers.root(), ps);
+     private static void dumpThreads(TextWriter writer) {
+         writer.println(processId());
+         writer.println(Instant.now());
+         writer.println(Runtime.version());
+         writer.println();
+         dumpThreads(ThreadContainers.root(), writer);
      }
  
-     private static void dumpThreads(ThreadContainer container, PrintStream ps) {
-         container.threads().forEach(t -> dumpThread(t, ps));
-         container.children().forEach(c -> dumpThreads(c, ps));
+     private static void dumpThreads(ThreadContainer container, TextWriter writer) {
+         container.threads().forEach(t -> dumpThread(t, writer));
+         container.children().forEach(c -> dumpThreads(c, writer));
      }
  
-     private static void dumpThread(Thread thread, PrintStream ps) {
-         String suffix = thread.isVirtual() ? " virtual" : "";
-         ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
-         for (StackTraceElement ste : thread.getStackTrace()) {
-             ps.print("      ");
-             ps.println(ste);
+     private static void dumpThread(Thread thread, TextWriter writer) {
+         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
+         Instant now = Instant.now();
+         Thread.State state = snapshot.threadState();
+         writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
+                 +  "\" " + state + " " + now);
+ 
+         // park blocker
+         Object parkBlocker = snapshot.parkBlocker();
+         if (parkBlocker != null) {
+             writer.print("      // parked on " + Objects.toIdentityString(parkBlocker));
+             if (parkBlocker instanceof AbstractOwnableSynchronizer
+                     && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
+                 writer.print(", owned by #" + owner.threadId());
+             }
+             writer.println();
+         }
+ 
+         // blocked on monitor enter or Object.wait
+         if (state == Thread.State.BLOCKED) {
+             Object obj = snapshot.blockedOn();
+             if (obj != null) {
+                 writer.println("      // blocked on " + Objects.toIdentityString(obj));
+             }
+         } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+             Object obj = snapshot.waitingOn();
+             if (obj != null) {
+                 writer.println("      // waiting on " + Objects.toIdentityString(obj));
+             }
          }
-         ps.println();
+ 
+         StackTraceElement[] stackTrace = snapshot.stackTrace();
+         int depth = 0;
+         while (depth < stackTrace.length) {
+             snapshot.ownedMonitorsAt(depth).forEach(obj -> {
+                 writer.print("      // locked ");
+                 writer.println(Objects.toIdentityString(obj));
+             });
+             writer.print("      ");
+             writer.println(stackTrace[depth]);
+             depth++;
+         }
+         writer.println();
      }
  
      /**
       * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
-      *
       * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
+      * @throws IOException if an I/O error occurs
       */
-     public static void dumpThreadsToJson(OutputStream out) {
-         BufferedOutputStream bos = new BufferedOutputStream(out);
-         PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
+     public static void dumpThreadsToJson(OutputStream out) throws IOException {
+         var writer = new TextWriter(out);
          try {
-             dumpThreadsToJson(ps);
-         } finally {
-             ps.flush();  // flushes underlying stream
+             dumpThreadsToJson(writer);
+             writer.flush();
+         } catch (UncheckedIOException e) {
+             IOException ioe = e.getCause();
+             throw ioe;
          }
      }
  
      /**
-      * Generate a thread dump to the given print stream in JSON format.
+      * Generate a thread dump to the given text stream in JSON format.
+      * @throws UncheckedIOException if an I/O error occurs
       */
-     private static void dumpThreadsToJson(PrintStream out) {
-         out.println("{");
-         out.println("  \"threadDump\": {");
- 
-         String now = Instant.now().toString();
-         String runtimeVersion = Runtime.version().toString();
-         out.format("    \"processId\": \"%d\",%n", processId());
-         out.format("    \"time\": \"%s\",%n", escape(now));
-         out.format("    \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
- 
-         out.println("    \"threadContainers\": [");
-         List<ThreadContainer> containers = allContainers();
-         Iterator<ThreadContainer> iterator = containers.iterator();
-         while (iterator.hasNext()) {
-             ThreadContainer container = iterator.next();
-             boolean more = iterator.hasNext();
-             dumpThreadsToJson(container, out, more);
-         }
-         out.println("    ]");   // end of threadContainers
- 
-         out.println("  }");   // end threadDump
-         out.println("}");  // end object
+     private static void dumpThreadsToJson(TextWriter textWriter) {
+         var jsonWriter = new JsonWriter(textWriter);
+ 
+         jsonWriter.startObject();  // top-level object
+ 
+         jsonWriter.startObject("threadDump");
+ 
+         jsonWriter.writeProperty("processId", processId());
+         jsonWriter.writeProperty("time", Instant.now());
+         jsonWriter.writeProperty("runtimeVersion", Runtime.version());
+ 
+         jsonWriter.startArray("threadContainers");
+         dumpThreads(ThreadContainers.root(), jsonWriter);
+         jsonWriter.endArray();
+ 
+         jsonWriter.endObject();  // threadDump
+ 
+         jsonWriter.endObject();  // end of top-level object
      }
  
      /**
-      * Dump the given thread container to the print stream in JSON format.
+      * Write a thread container to the given JSON writer.
+      * @throws UncheckedIOException if an I/O error occurs
       */
-     private static void dumpThreadsToJson(ThreadContainer container,
-                                           PrintStream out,
-                                           boolean more) {
-         out.println("      {");
-         out.format("        \"container\": \"%s\",%n", escape(container.toString()));
- 
-         ThreadContainer parent = container.parent();
-         if (parent == null) {
-             out.format("        \"parent\": null,%n");
-         } else {
-             out.format("        \"parent\": \"%s\",%n", escape(parent.toString()));
-         }
+     private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
+         jsonWriter.startObject();
+         jsonWriter.writeProperty("container", container);
+         jsonWriter.writeProperty("parent", container.parent());
  
          Thread owner = container.owner();
-         if (owner == null) {
-             out.format("        \"owner\": null,%n");
-         } else {
-             out.format("        \"owner\": \"%d\",%n", owner.threadId());
-         }
+         jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
  
          long threadCount = 0;
-         out.println("        \"threads\": [");
+         jsonWriter.startArray("threads");
          Iterator<Thread> threads = container.threads().iterator();
          while (threads.hasNext()) {
              Thread thread = threads.next();
-             dumpThreadToJson(thread, out, threads.hasNext());
+             dumpThread(thread, jsonWriter);
              threadCount++;
          }
-         out.println("        ],");   // end of threads
+         jsonWriter.endArray(); // threads
  
          // thread count
          if (!ThreadContainers.trackAllThreads()) {
              threadCount = Long.max(threadCount, container.threadCount());
          }
-         out.format("        \"threadCount\": \"%d\"%n", threadCount);
+         jsonWriter.writeProperty("threadCount", threadCount);
  
-         if (more) {
-             out.println("      },");
-         } else {
-             out.println("      }");  // last container, no trailing comma
-         }
+         jsonWriter.endObject();
+ 
+         // the children of the thread container follow
+         container.children().forEach(c -> dumpThreads(c, jsonWriter));
      }
  
      /**
-      * Dump the given thread and its stack trace to the print stream in JSON format.
+      * Write a thread to the given JSON writer.
+      * @throws UncheckedIOException if an I/O error occurs
       */
-     private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
-         out.println("         {");
-         out.println("           \"tid\": \"" + thread.threadId() + "\",");
-         out.println("           \"name\": \"" + escape(thread.getName()) + "\",");
-         out.println("           \"stack\": [");
- 
-         int i = 0;
-         StackTraceElement[] stackTrace = thread.getStackTrace();
-         while (i < stackTrace.length) {
-             out.print("              \"");
-             out.print(escape(stackTrace[i].toString()));
-             out.print("\"");
-             i++;
-             if (i < stackTrace.length) {
-                 out.println(",");
-             } else {
-                 out.println();  // last element, no trailing comma
+     private static void dumpThread(Thread thread, JsonWriter jsonWriter) {
+         Instant now = Instant.now();
+         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
+         Thread.State state = snapshot.threadState();
+         StackTraceElement[] stackTrace = snapshot.stackTrace();
+ 
+         jsonWriter.startObject();
+         jsonWriter.writeProperty("tid", thread.threadId());
+         jsonWriter.writeProperty("time", now);
+         if (thread.isVirtual()) {
+             jsonWriter.writeProperty("virtual", Boolean.TRUE);
+         }
+         jsonWriter.writeProperty("name", snapshot.threadName());
+         jsonWriter.writeProperty("state", state);
+ 
+         // park blocker
+         Object parkBlocker = snapshot.parkBlocker();
+         if (parkBlocker != null) {
+             jsonWriter.startObject("parkBlocker");
+             jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
+             if (parkBlocker instanceof AbstractOwnableSynchronizer
+                     && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
+                 jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId());
              }
+             jsonWriter.endObject();
          }
-         out.println("           ]");
-         if (more) {
-             out.println("         },");
-         } else {
-             out.println("         }");  // last thread, no trailing comma
+ 
+         // blocked on monitor enter or Object.wait
+         if (state == Thread.State.BLOCKED) {
+             Object obj = snapshot.blockedOn();
+             if (obj != null) {
+                 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
+             }
+         } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+             Object obj = snapshot.waitingOn();
+             if (obj != null) {
+                 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
+             }
          }
-     }
  
-     /**
-      * Returns a list of all thread containers that are "reachable" from
-      * the root container.
-      */
-     private static List<ThreadContainer> allContainers() {
-         List<ThreadContainer> containers = new ArrayList<>();
-         collect(ThreadContainers.root(), containers);
-         return containers;
-     }
+         // stack trace
+         jsonWriter.startArray("stack");
+         Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
+         jsonWriter.endArray();
+ 
+         // monitors owned, skip if none
+         if (snapshot.ownsMonitors()) {
+             jsonWriter.startArray("monitorsOwned");
+             int depth = 0;
+             while (depth < stackTrace.length) {
+                 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
+                 if (!objs.isEmpty()) {
+                     jsonWriter.startObject();
+                     jsonWriter.writeProperty("depth", depth);
+                     jsonWriter.startArray("locks");
+                     snapshot.ownedMonitorsAt(depth)
+                             .map(Objects::toIdentityString)
+                             .forEach(jsonWriter::writeProperty);
+                     jsonWriter.endArray();
+                     jsonWriter.endObject();
+                 }
+                 depth++;
+             }
+             jsonWriter.endArray();
+         }
+ 
+         // thread identifier of carrier, when mounted
+         if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
+             jsonWriter.writeProperty("carrier", carrier.threadId());
+         }
  
-     private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
-         containers.add(container);
-         container.children().forEach(c -> collect(c, containers));
+         jsonWriter.endObject();
      }
  
      /**
-      * Escape any characters that need to be escape in the JSON output.
+      * Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
+      * This class is not intended to be a fully featured JSON writer.
       */
-     private static String escape(String value) {
-         StringBuilder sb = new StringBuilder();
-         for (int i = 0; i < value.length(); i++) {
-             char c = value.charAt(i);
-             switch (c) {
-                 case '"'  -> sb.append("\\\"");
-                 case '\\' -> sb.append("\\\\");
-                 case '/'  -> sb.append("\\/");
-                 case '\b' -> sb.append("\\b");
-                 case '\f' -> sb.append("\\f");
-                 case '\n' -> sb.append("\\n");
-                 case '\r' -> sb.append("\\r");
-                 case '\t' -> sb.append("\\t");
-                 default -> {
-                     if (c <= 0x1f) {
-                         sb.append(String.format("\\u%04x", c));
-                     } else {
-                         sb.append(c);
+     private static class JsonWriter {
+         private static class Node {
+             final boolean isArray;
+             int propertyCount;
+             Node(boolean isArray) {
+                 this.isArray = isArray;
+             }
+             boolean isArray() {
+                 return isArray;
+             }
+             int propertyCount() {
+                 return propertyCount;
+             }
+             int getAndIncrementPropertyCount() {
+                 int old = propertyCount;
+                 propertyCount++;
+                 return old;
+             }
+         }
+         private final Deque<Node> stack = new ArrayDeque<>();
+         private final TextWriter writer;
+ 
+         JsonWriter(TextWriter writer) {
+             this.writer = writer;
+         }
+ 
+         private void indent() {
+             int indent = stack.size() * 2;
+             writer.print(" ".repeat(indent));
+         }
+ 
+         /**
+          * Start of object or array.
+          */
+         private void startObject(String name, boolean isArray) {
+             if (!stack.isEmpty()) {
+                 Node node = stack.peek();
+                 if (node.getAndIncrementPropertyCount() > 0) {
+                     writer.println(",");
+                 }
+             }
+             indent();
+             if (name != null) {
+                 writer.print("\"" + name + "\": ");
+             }
+             writer.println(isArray ? "[" : "{");
+             stack.push(new Node(isArray));
+         }
+ 
+         /**
+          * End of object or array.
+          */
+         private void endObject(boolean isArray) {
+             Node node = stack.pop();
+             if (node.isArray() != isArray)
+                 throw new IllegalStateException();
+             if (node.propertyCount() > 0) {
+                 writer.println();
+             }
+             indent();
+             writer.print(isArray ? "]" : "}");
+         }
+ 
+         /**
+          * Write a property.
+          * @param name the property name, null for an unnamed property
+          * @param obj the value or null
+          */
+         void writeProperty(String name, Object obj) {
+             Node node = stack.peek();
+             if (node.getAndIncrementPropertyCount() > 0) {
+                 writer.println(",");
+             }
+             indent();
+             if (name != null) {
+                 writer.print("\"" + name + "\": ");
+             }
+             switch (obj) {
+                 // Long may be larger than safe range of JSON integer value
+                 case Long   _  -> writer.print("\"" + obj + "\"");
+                 case Number _  -> writer.print(obj);
+                 case Boolean _ -> writer.print(obj);
+                 case null      -> writer.print("null");
+                 default        -> writer.print("\"" + escape(obj.toString()) + "\"");
+             }
+         }
+ 
+         /**
+          * Write an unnamed property.
+          */
+         void writeProperty(Object obj) {
+             writeProperty(null, obj);
+         }
+ 
+         /**
+          * Start named object.
+          */
+         void startObject(String name) {
+             startObject(name, false);
+         }
+ 
+         /**
+          * Start unnamed object.
+          */
+         void startObject() {
+             startObject(null);
+         }
+ 
+         /**
+          * End of object.
+          */
+         void endObject() {
+             endObject(false);
+         }
+ 
+         /**
+          * Start named array.
+          */
+         void startArray(String name) {
+             startObject(name, true);
+         }
+ 
+         /**
+          * End of array.
+          */
+         void endArray() {
+             endObject(true);
+         }
+ 
+         /**
+          * Escape any characters that need to be escape in the JSON output.
+          */
+         private static String escape(String value) {
+             StringBuilder sb = new StringBuilder();
+             for (int i = 0; i < value.length(); i++) {
+                 char c = value.charAt(i);
+                 switch (c) {
+                     case '"'  -> sb.append("\\\"");
+                     case '\\' -> sb.append("\\\\");
+                     case '/'  -> sb.append("\\/");
+                     case '\b' -> sb.append("\\b");
+                     case '\f' -> sb.append("\\f");
+                     case '\n' -> sb.append("\\n");
+                     case '\r' -> sb.append("\\r");
+                     case '\t' -> sb.append("\\t");
+                     default -> {
+                         if (c <= 0x1f) {
+                             sb.append(String.format("\\u%04x", c));
+                         } else {
+                             sb.append(c);
+                         }
                      }
                  }
              }
+             return sb.toString();
          }
-         return sb.toString();
      }
  
      /**
       * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
       * written the subsequent bytes are discarded.

@@ -355,10 +562,60 @@
          @Override
          public void close() {
          }
      }
  
+     /**
+      * Simple Writer implementation for printing text. The print/println methods
+      * throw UncheckedIOException if an I/O error occurs.
+      */
+     private static class TextWriter extends Writer {
+         private final Writer delegate;
+ 
+         TextWriter(OutputStream out) {
+             delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+         }
+ 
+         @Override
+         public void write(char[] cbuf, int off, int len) throws IOException {
+             delegate.write(cbuf, off, len);
+         }
+ 
+         void print(Object obj) {
+             String s = String.valueOf(obj);
+             try {
+                 write(s, 0, s.length());
+             } catch (IOException ioe) {
+                 throw new UncheckedIOException(ioe);
+             }
+         }
+ 
+         void println() {
+             print(System.lineSeparator());
+         }
+ 
+         void println(String s) {
+             print(s);
+             println();
+         }
+ 
+         void println(Object obj) {
+             print(obj);
+             println();
+         }
+ 
+         @Override
+         public void flush() throws IOException {
+             delegate.flush();
+         }
+ 
+         @Override
+         public void close() throws IOException {
+             delegate.close();
+         }
+     }
+ 
      /**
       * Returns the process ID or -1 if not supported.
       */
      private static long processId() {
          try {
< prev index next >