Java ProcessBuilder & Process API: Running OS Commands from Java
Java's Process API lets you launch external OS processes, capture their output, supply input, and monitor their lifecycle from within JVM code. ProcessBuilder configures the command, working directory, environment variables, and I/O redirection. ProcessHandle (Java 9+) lets you query and kill processes by PID, list descendants, and react when a process terminates via a CompletableFuture. This article covers both APIs with practical examples.
Why the Process API Exists
Some tasks simply cannot be done from within the JVM: running a shell script, invoking gitffmpegpandoc
The original mechanism was Runtime.exec(String command)Process
ProcessBuilder: Configuring and Starting a Process
A ProcessBuilderProcessBuilderdirectory()environment()redirectErrorStream(true)
Calling start()ProcesswaitFor()waitFor(timeout, unit)
import java.io.*;
import java.util.List;
// Run: git log --oneline -5
ProcessBuilder pb = new ProcessBuilder(
"git", "log", "--oneline", "-5"
);
pb.directory(new File("/path/to/repo")); // working directory
pb.redirectErrorStream(true); // merge stderr into stdout
Process process = pb.start();
// Read all output before calling waitFor() — see next section for why
String output;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
output = reader.lines()
.collect(java.util.stream.Collectors.joining("\n"));
}
int exitCode = process.waitFor();
System.out.println("Exit code: " + exitCode);
System.out.println(output);
ProcessBuilder|>*~new ProcessBuilder("sh", "-c", "ls *.java | wc -l")Reading stdout and stderr: Avoiding Deadlock
A child process writes its output to two streams: stdout and stderr. Each stream has a fixed-size OS pipe buffer (typically 64 KB on Linux). If your Java code calls waitFor()waitFor()
The safest solution is to consume stdout and stderr in separate threads, or redirect stderr to a file/INHERIT/DISCARD before calling waitFor()redirectErrorStream(true)
ProcessBuilder pb = new ProcessBuilder("javac", "BadCode.java");
pb.directory(new File("/tmp"));
// Redirect stderr to a file — no need to consume it manually
pb.redirectError(new File("/tmp/compile-errors.txt"));
Process proc = pb.start();
// Read stdout in the calling thread (stderr goes to file)
String stdout;
try (var reader = new BufferedReader(
new InputStreamReader(proc.getInputStream()))) {
stdout = reader.lines().collect(java.util.stream.Collectors.joining("\n"));
}
int exit = proc.waitFor();
System.out.println("Compiler exit: " + exit);
// --- Alternative: consume both streams concurrently with threads ---
Process proc2 = new ProcessBuilder("some-verbose-tool").start();
Thread stdoutReader = Thread.ofVirtual().start(() -> {
try (var r = new BufferedReader(new InputStreamReader(proc2.getInputStream()))) {
r.lines().forEach(System.out::println);
} catch (IOException e) { /* handle */ }
});
Thread stderrReader = Thread.ofVirtual().start(() -> {
try (var r = new BufferedReader(new InputStreamReader(proc2.getErrorStream()))) {
r.lines().forEach(System.err::println);
} catch (IOException e) { /* handle */ }
});
proc2.waitFor();
stdoutReader.join();
stderrReader.join();
waitFor()Sending Input to a Process
The stdin of a child process is exposed as an OutputStreamprocess.getOutputStream()
// Pipe text through a command that reads stdin (e.g., wc -w to count words)
ProcessBuilder pb = new ProcessBuilder("wc", "-w");
pb.redirectErrorStream(true);
Process process = pb.start();
// Write to the process's stdin in a separate thread
// (to avoid deadlock when output fills the buffer before we finish writing)
String input = "The quick brown fox jumps over the lazy dog";
Thread writer = Thread.ofVirtual().start(() -> {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(process.getOutputStream()))) {
pw.println(input);
} // closing the stream signals EOF to the process
});
String output;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
output = reader.lines().collect(java.util.stream.Collectors.joining());
}
writer.join();
process.waitFor();
System.out.println("Word count: " + output.trim()); // 9
ProcessHandle: Querying and Managing Processes (Java 9+)
ProcessHandleProcessHandleCompletableFuture
The ProcessHandle.InfoOptional
import java.lang.ProcessHandle;
import java.time.ZoneId;
// Current JVM process
ProcessHandle current = ProcessHandle.current();
System.out.println("JVM PID: " + current.pid());
ProcessHandle.Info info = current.info();
info.command().ifPresent(cmd -> System.out.println("Command: " + cmd));
info.startInstant().ifPresent(t ->
System.out.println("Started: " + t.atZone(ZoneId.systemDefault())));
// Look up a process by PID
ProcessHandle.of(1234).ifPresent(ph -> {
System.out.println("Process 1234 alive: " + ph.isAlive());
ph.destroy(); // SIGTERM
// ph.destroyForcibly(); // SIGKILL
});
// React when a child process finishes
Process child = new ProcessBuilder("sleep", "2").start();
child.toHandle()
.onExit()
.thenAccept(ph -> System.out.println("Process " + ph.pid() + " finished"));
// List all children of the current process
ProcessHandle.current().children()
.forEach(ph -> System.out.println("Child PID: " + ph.pid()));
Process Pipelines: Chaining Commands (Java 9+)
In a Unix shell, the pipe operator |ProcessBuilder.startPipeline()ProcessBuilderProcess
import java.util.List;
// Equivalent of: ls | grep .java | sort
List<Process> pipeline = ProcessBuilder.startPipeline(List.of(
new ProcessBuilder("ls"),
new ProcessBuilder("grep", ".java"),
new ProcessBuilder("sort")
));
// Only the last process has output we need to read
Process last = pipeline.get(pipeline.size() - 1);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(last.getInputStream()))) {
reader.lines().forEach(System.out::println);
}
// Wait for all processes to finish
for (Process p : pipeline) {
p.waitFor();
}
Processdestroy()waitFor()