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
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
- relates to
-
JDK-8297451 ProcessHandleImpl should assert privilege when modifying reaper thread
-
- Closed
-