Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8279488

ProcessBuilder inherits contextClassLoader when spawning a process reaper thread

    XMLWordPrintable

Details

    • b16
    • generic
    • generic
    • Verified

    Description

      ADDITIONAL SYSTEM INFORMATION :
      System: Macbook Pro 16 (2019)
      OS: mac 11.5.1
      JDKs:
      - OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9)
      - OpenJDK Runtime Environment Temurin-17.0.1+12 (build 17.0.1+12)

      A DESCRIPTION OF THE PROBLEM :
      The ProcessBuilder makes use of a cached ThreadPoolExecutor internally to monitor the status of the forked processes. Whenever a new thread named "process reaper" is created within this pool through the defined ThreadFactory, this new thread inherits the contextClassLoader of the calling thread (default thread creation behavior).

      As a result, since the "process reaper" threads can stay as long as there are processes to execute (keep alive time is set to 1 mn in java.util.concurrent.Executors.newCachedThreadPool()), such a thread retains a reference to the contextClassLoader instance of the thread which triggered the new "process reaper" thread allocation in the first place.

      Such a reference to this contextClassLoader implies that the entire object graph attached to it is retained in memory and cannot be garbage-collected, even if otherwise discarded: the initial situation which triggered this investigation was indeed a classloader leak happening when a webapp making use of ProcessBuilder is restarted within a Tomcat container.

      In addition to this, since these threads are shared and reused across the JVM and cannot allocate non java.lang objects, it does not look relevant to use a specific contextClassLoader for the "process reaper" threads in any case.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Build the provided test class, and run it.
      This class executes a process twice, each time with a different contextClassLoader, reports the outcome and the current contextClassLoader of all the "process reaper" threads.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The contextClassLoader of idle "process reaper" threads should be the bootstrap classloader (null):

      Process #32998 launched using classloader java.net.URLClassLoader@7344699f - exit code 0
      Thread process reaper #22 - contextClassLoader null
      Process #32999 launched using classloader java.net.URLClassLoader@2a84aee7 - exit code 0
      Thread process reaper #22 - contextClassLoader null
      ACTUAL -
      The contextClassLoader of idle "process reaper" threads is the contextClassLoader instance used during the first run instead:

      Process #32998 launched using classloader java.net.URLClassLoader@7344699f - exit code 0
      Thread process reaper #22 - contextClassLoader java.net.URLClassLoader@7344699f <<<<< HERE
      Process #32999 launched using classloader java.net.URLClassLoader@2a84aee7 - exit code 0
      Thread process reaper #22 - contextClassLoader java.net.URLClassLoader@7344699f <<<<< HERE

      ---------- BEGIN SOURCE ----------
      import java.io.File;
      import java.io.IOException;
      import java.net.URL;
      import java.net.URLClassLoader;
      import java.util.concurrent.CompletableFuture;

      public class ProcessBuilderClassloadingIssue {
          
          public static void main(String... args) throws Exception {
              // Run #1
              execProcessUsingClassLoaderAndReport(new ProcessLauncher(), newClassLoader());
              reportProcessReaperThreads();

              // Run #2
              execProcessUsingClassLoaderAndReport(new ProcessLauncher(), newClassLoader());
              reportProcessReaperThreads();
          }

          public static ClassLoader newClassLoader() throws Exception {
              return new URLClassLoader(new URL[]{ new File(".").toURI().toURL() });
          }
          
          public static void execProcessUsingClassLoaderAndReport(
                  ProcessLauncher processLauncher,
                  ClassLoader classLoader) throws Exception {
              // Switch the current thread contextClassLoader
              Thread.currentThread().setContextClassLoader(classLoader);
              
              Process process = processLauncher.exec().get();
              System.out.println("Process #" + process.pid() + " launched using classloader "
                      + classLoader + " - exit code " + process.exitValue());
          }
          
          public static void reportProcessReaperThreads() {
              for (Thread thread: Thread.getAllStackTraces().keySet()) {
                  if ("process reaper".equals(thread.getName())) {
                      System.out.println("Thread " + thread.getName() + " #" + thread.getId()
                              + " - contextClassLoader " + thread.getContextClassLoader() );
                  }
              }
          }
          
          public static class ProcessLauncher {
              public static final String COMMAND = "/bin/date";
              
              public CompletableFuture<Process> exec() throws IOException {
                  return new ProcessBuilder(COMMAND).start().onExit();
              }
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      None found.

      FREQUENCY : always


      Attachments

        Issue Links

          Activity

            People

              rriggs Roger Riggs
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: