<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Haider Kagalwala</title>
    <description>The latest articles on DEV Community by Haider Kagalwala (@haider_hussainkagalwala_).</description>
    <link>https://dev.to/haider_hussainkagalwala_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3831509%2Ff148df78-5c47-47a7-878a-4c96cb35f8b6.jpg</url>
      <title>DEV Community: Haider Kagalwala</title>
      <link>https://dev.to/haider_hussainkagalwala_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/haider_hussainkagalwala_"/>
    <language>en</language>
    <item>
      <title>Java ProcessBuilder: Deadlocks, Zombies, and the 64KB Wall</title>
      <dc:creator>Haider Kagalwala</dc:creator>
      <pubDate>Sat, 21 Mar 2026 09:20:34 +0000</pubDate>
      <link>https://dev.to/haider_hussainkagalwala_/java-processbuilder-deadlocks-zombies-and-the-64kb-wall-5hn7</link>
      <guid>https://dev.to/haider_hussainkagalwala_/java-processbuilder-deadlocks-zombies-and-the-64kb-wall-5hn7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published in Level Up Coding. You can read the original version &lt;a href="https://levelup.gitconnected.com/java-processbuilder-deadlocks-zombies-and-the-64kb-wall-8f754bc15bbc" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently at IBM Software Labs, I worked on a task that forced me to understand something many Java developers rarely think about — how Java interacts with the operating system.&lt;/p&gt;

&lt;p&gt;Most of our daily work happens safely inside the JVM. Memory management, threads, and file handling — the JVM abstracts these away nicely.&lt;/p&gt;

&lt;p&gt;But sometimes you need to step outside. You want to run a shell script, invoke a system binary, or trigger a native tool that no Java library wraps. This is where &lt;code&gt;ProcessBuilder&lt;/code&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt; is the modern Java API for executing native OS commands from Java code. But the moment you call &lt;code&gt;pb.start()&lt;/code&gt;, you leave the JVM's safe world. What follows is deadlocks, zombie processes, file descriptor leakage, and race conditions — OS-level problems the JVM cannot protect you from.&lt;/p&gt;

&lt;h1&gt;
  
  
  ProcessBuilder: The Basics
&lt;/h1&gt;

&lt;p&gt;Before we go any deeper, let me show you what ProcessBuilder actually looks like. At its simplest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ls"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-la"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Start it - this is where Java hands control to the OS&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Read the output&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Wait for it to finish and get the exit code&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exited with: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. But here is the thing — that single line &lt;code&gt;pb.start()&lt;/code&gt; is doing far more than it looks. The moment you call it, you have left the JVM. You are now in the OS's territory; the OS has its own rules.&lt;/p&gt;

&lt;p&gt;To understand what those rules are — and why breaking them causes deadlocks, zombies, and resource exhaustion — you need to understand what Linux is actually doing underneath that one method call.&lt;/p&gt;

&lt;h1&gt;
  
  
  Fundamentals: Life Outside the JVM
&lt;/h1&gt;

&lt;p&gt;Before we talk about Java code, we have to talk about how Linux actually breathes. If you don't understand the ground your Java process is standing on, the problems we're about to cover will feel random. They aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything in Linux Is a Process
&lt;/h2&gt;

&lt;p&gt;In Linux, almost everything that does something is a process. Every program, every command, every background service — all of it is a process with a unique ID called a PID.&lt;/p&gt;

&lt;p&gt;It all starts with &lt;code&gt;systemd&lt;/code&gt; — PID 1. The ancestor of every process on your machine. When you call &lt;code&gt;pb.start()&lt;/code&gt; in Java, you aren't just running a command. You are asking the Kernel to birth a new child process, descended from your Java process, descended from systemd.&lt;/p&gt;

&lt;p&gt;You can see this lineage yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pstree &lt;span class="nt"&gt;-p&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4atmwaw06r0lpqnssw4r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4atmwaw06r0lpqnssw4r.png" alt="process tree in linux"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at that tree. &lt;code&gt;systemd(1)&lt;/code&gt; at the root. Follow it down — &lt;code&gt;init-systemd → SessionLeader → Relay → bash → jshell → java&lt;/code&gt;. Every process has a parent. Every process belongs to a lineage. When you call &lt;code&gt;pb.start()&lt;/code&gt;, your new process gets added to this tree as a child of your Java process.&lt;/p&gt;

&lt;p&gt;This parent-child relationship isn't just cosmetic. It's the mechanism the OS uses to track accountability. &lt;strong&gt;The parent is responsible for its children.&lt;/strong&gt; As we'll see in the zombie section, when a parent fails to acknowledge a child's death, the OS has nowhere to put the exit status. The child lingers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork and Exec: How a Process Is Actually Born
&lt;/h2&gt;

&lt;p&gt;So what does the OS actually do when &lt;code&gt;pb.start()&lt;/code&gt; is called? It performs a two-step operation every single time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fork.&lt;/strong&gt; The OS creates an exact twin of your Java process. For a split second, two identical Java processes exist in memory. Thanks to a technique called Copy-on-Write, this doesn't actually double your memory — both processes share the same RAM pages until one of them modifies something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exec.&lt;/strong&gt; The twin then replaces itself entirely. It wipes its own memory, discards the Java bytecode, and loads the binary of the command you passed to ProcessBuilder — &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, whatever it is. The twin is gone. The new process has taken its place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why &lt;code&gt;pb.start()&lt;/code&gt; feels instantaneous. You aren't building a new process from scratch — you're cloning and replacing. The OS has been doing this billions of times a day since the 1970s.&lt;/p&gt;

&lt;h2&gt;
  
  
  /proc: The Kernel's Brain
&lt;/h2&gt;

&lt;p&gt;There's a directory on every Linux system called &lt;code&gt;/proc&lt;/code&gt;. It looks like a folder. It isn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /proc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flby00cs47ikzrxsz9448.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flby00cs47ikzrxsz9448.png" alt="/proc directory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing you see there lives on your hard drive. &lt;code&gt;/proc&lt;/code&gt; is a &lt;strong&gt;virtual filesystem&lt;/strong&gt; — a live window the Kernel exposes so you can inspect what's happening inside it in real time. Every numbered directory you see is a running process. Every file inside it is a piece of that process's live state, rendered on demand by the Kernel the moment you read it.&lt;/p&gt;

&lt;p&gt;If your Java process has PID 1234, everything about it lives at &lt;code&gt;/proc/1234/&lt;/code&gt; — its memory maps, its open file handles, its current working directory, the exact command that launched it. No special tooling required. The Kernel is just telling you, right there in the filesystem.&lt;/p&gt;

&lt;p&gt;We'll come back to &lt;code&gt;/proc&lt;/code&gt; throughout this article. Once you know it's there, you'll never debug a process-related issue the same way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Descriptors: The Ticket Numbers
&lt;/h2&gt;

&lt;p&gt;Linux follows a foundational philosophy — &lt;strong&gt;everything is a file&lt;/strong&gt;. A document on disk, a network socket, a pipe between two processes — the Kernel treats all of it uniformly as a file and hands your process a number to reference it. That number is a &lt;strong&gt;File Descriptor&lt;/strong&gt;, or FD.&lt;/p&gt;

&lt;p&gt;Think of FDs as ticket numbers at a Dosa counter. When your process wants to interact with the outside world — read a file, write to a socket, receive input — it hands the Kernel a ticket number. The Kernel knows what that number maps to and routes the operation accordingly.&lt;/p&gt;

&lt;p&gt;You can see exactly which FDs your process currently holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ll&lt;/span&gt; /proc/&amp;lt;PID&amp;gt;/fd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91zwllqfu96flzxp5foz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91zwllqfu96flzxp5foz.png" alt="File Descriptors"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at what's already there before your process does anything meaningful — FD 0, 1, and 2, already open, already pointing somewhere. Those are the Big Three, and every process on Linux starts life with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  STDIN, STDOUT, STDERR: The Three Doors
&lt;/h2&gt;

&lt;p&gt;Every process starts with three FDs already open:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FD 0 — STDIN.&lt;/strong&gt; The ear. This is where the process listens for input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FD 1 — STDOUT.&lt;/strong&gt; The mouth. This is where the process sends its normal output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FD 2 — STDERR.&lt;/strong&gt; The megaphone. This is where the process reports errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those three FDs are pointing to &lt;code&gt;pipe:[1833473]&lt;/code&gt;, &lt;code&gt;pipe:[1833474]&lt;/code&gt;, &lt;code&gt;pipe:[1833475]&lt;/code&gt;. That's because this process was spawned by Java using &lt;code&gt;ProcessBuilder&lt;/code&gt;. The Kernel has wired those three doors directly to the parent Java process, creating a private channel between them.&lt;/p&gt;

&lt;p&gt;When you use &lt;code&gt;ProcessBuilder&lt;/code&gt;, your Java code holds the other end of each of these pipes. If you don't manage those connections carefully — drain them, close them, acknowledge them — those pipes back up, those FDs accumulate, and your application hits a wall. That's exactly what the rest of this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limits the Kernel Enforces
&lt;/h2&gt;

&lt;p&gt;The Kernel doesn't let any of this run unchecked. Two hard limits govern how far your process can go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FD limits.&lt;/strong&gt; The OS caps how many file descriptors a single process — and a single user — can hold open simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;   &lt;span class="c"&gt;# soft limit - what your process currently sees&lt;/span&gt;
&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-Hn&lt;/span&gt;  &lt;span class="c"&gt;# hard limit - the ceiling the admin set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PID limits.&lt;/strong&gt; PIDs aren't infinite either. The Kernel has a maximum number of processes that can exist on the system at any given time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/kernel/pid_max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers feel large until they don't. Exhaust your FD limit and your JVM can't open log files, can't accept network connections, can't spawn new processes. Exhaust your PID limit and the entire system stops being able to create new processes — not just your app, everything on the machine.&lt;/p&gt;

&lt;p&gt;Keep these limits in the back of your mind. By the time we reach the FD leakage section, you'll see exactly how a few lines of careless Java code can push a production server into either of these walls.&lt;/p&gt;

&lt;p&gt;Now you know what the OS is managing underneath every &lt;code&gt;pb.start()&lt;/code&gt; call — processes in a tree, pipes as file descriptors, three doors already open, hard limits enforced by the Kernel.&lt;/p&gt;

&lt;p&gt;With that foundation in place, let's look at what happens when things go wrong. And the first place things go wrong — quietly, invisibly, and almost always in production — is the buffer.&lt;/p&gt;

&lt;h1&gt;
  
  
  The 64KB Wall: How Your App Freezes Itself
&lt;/h1&gt;

&lt;p&gt;This is the 3 AM bug. Works perfectly on your machine. Passes every local test. The moment it hits production with real-world output, your application freezes — no exception, no stack trace, no warning. Just silence.&lt;/p&gt;

&lt;p&gt;To understand why, you have to understand what the OS actually builds when you call &lt;code&gt;pb.start()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When Java spawns a child process, the Kernel creates pipes between them for STDOUT and STDERR. Think of each pipe as a small physical bucket sitting in RAM — on modern Linux, each bucket holds about &lt;strong&gt;64KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As your child process runs, it pours output into that bucket. Your Java app is supposed to sit on the other side, continuously draining it. This works fine, until it doesn't.&lt;/p&gt;

&lt;p&gt;When the bucket fills up, the Linux Kernel does something brutal. It &lt;strong&gt;freezes the child process&lt;/strong&gt;. Pauses it mid-execution and says: &lt;em&gt;"You don't get to write another byte until someone empties this bucket."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now here's where the deadlock is born.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Java thread     → waitFor() → sleeping, waiting for the child to exit
Child process   → bucket full → sleeping, waiting for Java to drain it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides are asleep. Both waiting for the other to move first. Neither ever will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching It Happen
&lt;/h2&gt;

&lt;p&gt;Let's not just talk about it. Here's the deadlock code running live in JShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"echo Child PID: $$; for i in {1..100000}; do echo Line $i; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Parent PID: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;ProcessHandle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Started child process with PID: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Deadlocked...go to another terminal"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Intentionally not draining stdout — deadlock for &amp;gt; 64KB&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Child exited with code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3yfi6mmw1k0a71l1czg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3yfi6mmw1k0a71l1czg.png" alt="Deadlock"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The program printed three lines and stopped. The child is generating output. The parent is stuck in &lt;code&gt;waitFor()&lt;/code&gt;. Neither is moving. The application is completely frozen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What /proc Is Telling You
&lt;/h2&gt;

&lt;p&gt;While the parent is frozen, open another terminal and look inside &lt;code&gt;/proc&lt;/code&gt;. This is where it gets interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ll&lt;/span&gt; /proc/89807/fd/   &lt;span class="c"&gt;# child process&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ll&lt;/span&gt; /proc/89718/fd/   &lt;span class="c"&gt;# parent Java process&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwd7ypt789orxpekw8mia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwd7ypt789orxpekw8mia.png" alt="/proc Live Deadlock for 64KB Pipes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look carefully at what the Kernel is showing you.&lt;/p&gt;

&lt;p&gt;The child process 89807 has FD 1 — its STDOUT — pointing to &lt;code&gt;pipe:[1840573]&lt;/code&gt;. That's the write end. The child is pouring 100,000 lines into this pipe and has nowhere else to go.&lt;/p&gt;

&lt;p&gt;The parent Java process 89718 has FD 9 pointing to &lt;code&gt;pipe:[1840573]&lt;/code&gt; — &lt;strong&gt;&lt;em&gt;the read end of that exact same pipe&lt;/em&gt;&lt;/strong&gt;. It is supposed to be draining that bucket. But it called &lt;code&gt;waitFor()&lt;/code&gt; first and is now sleeping, waiting for the child to exit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same pipe number. Two ends. Neither moving.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Kernel is showing you the deadlock right there in &lt;code&gt;/proc&lt;/code&gt;. Two processes connected by a pipe — one frozen waiting to write, one frozen waiting for exit. This is not a Java bug, not a logic error. This is what happens when you ask the OS to hold data nobody is reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Release
&lt;/h2&gt;

&lt;p&gt;Now, drain the pipe externally from that second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/89807/fd/1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwk9e92rehj2sugcyuqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwk9e92rehj2sugcyuqh.png" alt="Draining the Pipe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The moment the bucket starts emptying, the child wakes up, finishes writing, and exits. The parent's &lt;code&gt;waitFor()&lt;/code&gt; returns. The application that was frozen solid printed its exit code and came back to life — all because someone finally emptied the bucket.&lt;/p&gt;

&lt;p&gt;That's the deadlock. That's the release. And that's exactly what your Java code is supposed to be doing automatically — draining that pipe while the process runs, not waiting until after it exits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on STDERR throughout these examples:&lt;/strong&gt; Every fix below uses &lt;code&gt;pb.redirectErrorStream(true)&lt;/code&gt;, which merges STDERR into STDOUT at the OS level before data even reaches Java. This keeps the examples focused. In a real application where you need both streams separately, you must drain STDOUT and STDERR &lt;strong&gt;concurrently&lt;/strong&gt; — we cover exactly that in Fix 2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fix 1: BufferedReader — The Simplest Drain
&lt;/h2&gt;

&lt;p&gt;The most straightforward fix: read the output line by line before calling &lt;code&gt;waitFor()&lt;/code&gt;. A &lt;code&gt;BufferedReader&lt;/code&gt; continuously pulls from the pipe, draining the bucket so the child process never hits that wall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"for i in {1..100000}; do echo \"Line $i\"; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Drain the pipe first — the bucket never fills up&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Output: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// By the time we get here the process has already finished.&lt;/span&gt;
&lt;span class="c1"&gt;// waitFor() just collects the exit code&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Child exited with code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, readable, and it works. But your main thread is blocked the entire time the process is running. For short-lived commands, this is perfectly fine. For anything long-running or high-volume, you want the async approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Async Consumption with CompletableFuture
&lt;/h2&gt;

&lt;p&gt;Instead of blocking your main thread, spin up a background task that drains the pipe concurrently while the process runs. Your thread stays free to do other work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"for i in {1..100000}; do echo \"Line $i\"; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Drain on a background thread - runs concurrently with the process&lt;/span&gt;
&lt;span class="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;drainTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runAsync&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PID "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" | OUT: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Read error for PID "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// onExit() fires when the process terminates.&lt;/span&gt;
&lt;span class="c1"&gt;// thenCompose waits for the drainer to finish consuming whatever&lt;/span&gt;
&lt;span class="c1"&gt;// is left in the buffer - only then do we handle the exit code.&lt;/span&gt;
&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onExit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenCompose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;drainTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenApply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PID "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" exited with code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exitValue&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Process launched - main thread is free"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onExit()&lt;/code&gt; fires when the process terminates — but that doesn't mean the pipe is empty. The child may have written data still sitting in the buffer. &lt;code&gt;thenCompose&lt;/code&gt; chains the exit notification to wait for &lt;code&gt;drainTask&lt;/code&gt; to fully finish before we touch the exit code. The &lt;code&gt;try-with-resources&lt;/code&gt; inside the drainer closes STDOUT automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need the exit code inline&lt;/strong&gt;, skip &lt;code&gt;onExit()&lt;/code&gt; and use the blocking version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;drainTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// wait for drain to complete&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns instantly — process is already done&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exited with code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;join()&lt;/code&gt; here is purely about ordering — not FD cleanup, not zombie prevention. By the time the drain finishes, the process has already exited. &lt;code&gt;waitFor()&lt;/code&gt; just collects the exit code at that point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: redirectErrorStream — One Pipe, One Drainer
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't need STDOUT and STDERR as separate streams. &lt;code&gt;redirectErrorStream(true)&lt;/code&gt; tells the OS to merge STDERR into STDOUT before it even reaches Java. One bucket, one drainer, one less thing to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"echo 'normal output'; echo 'error output' &amp;gt;&amp;amp;2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// STDERR folded into STDOUT at the OS level&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// both streams arrive here&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right call when you just want everything in one place for logging and don't need to tell the two streams apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: inheritIO — Let the OS Handle It
&lt;/h2&gt;

&lt;p&gt;Sometimes the best way to manage a stream is to not manage it at all. &lt;code&gt;inheritIO()&lt;/code&gt; tells the Kernel to wire the child's pipes directly to your terminal. No bucket in the JVM, no drainer thread, no deadlock risk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"for i in {1..100000}; do echo \"Line $i\"; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inheritIO&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// stdout and stderr go straight to the terminal&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// safe - nothing to clog&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exit code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect during development or for CLI tools. The tradeoff: you lose access to the output inside Java entirely. It flows straight to the terminal and that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 5: File Redirect — Let the OS Write to Disk
&lt;/h2&gt;

&lt;p&gt;Same principle as &lt;code&gt;inheritIO&lt;/code&gt;, but for production. The OS writes output directly to a file — the JVM never touches the data, no buffer to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"for i in {1..100000}; do echo \"Line $i\"; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/somedir/stdout.log"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/somedir/stderr.log"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// safe - OS draining directly to disk&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exit code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean and efficient for persistent logging. Same tradeoff as &lt;code&gt;inheritIO&lt;/code&gt; — you can't process the output in Java code, but for a proper reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 6: Discard — When You Don't Care At All
&lt;/h2&gt;

&lt;p&gt;When output is genuinely irrelevant, don't create a pipe in the first place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"for i in {1..100000}; do echo \"Line $i\"; done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Redirect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DISCARD&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// safe - no buffer exists&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exit code: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Redirect.DISCARD&lt;/code&gt; handles this at the OS level — no pipe is created, no bucket to fill, nothing to manage. Cleaner than draining into &lt;code&gt;OutputStream.nullOutputStream()&lt;/code&gt; in Java because the data never enters the JVM at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Fix Should You Actually Use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short command, output needed in Java&lt;/td&gt;
&lt;td&gt;BufferedReader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-running command, main thread must stay free&lt;/td&gt;
&lt;td&gt;Async CompletableFuture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need exit code inline, sequentially&lt;/td&gt;
&lt;td&gt;Async drain + join() + waitFor()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All output in one place, no need to separate streams&lt;/td&gt;
&lt;td&gt;redirectErrorStream + BufferedReader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development, debugging, or CLI tools&lt;/td&gt;
&lt;td&gt;inheritIO()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production logging, no processing needed in Java&lt;/td&gt;
&lt;td&gt;File redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output is completely irrelevant&lt;/td&gt;
&lt;td&gt;Redirect.DISCARD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The one rule that holds across every single case: **the pipe must have somewhere to go before you call &lt;code&gt;waitFor()&lt;/code&gt;&lt;/em&gt;&lt;em&gt;. Whether that's a reader thread, a file, or the terminal — the bucket must never be left to fill up.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Zombie Processes: The Dead That Won't Leave
&lt;/h1&gt;

&lt;p&gt;Your process finished. The command ran, the work is done. But something is still sitting in the OS process table, consuming a PID, marked with a haunting label in &lt;code&gt;ps&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID   PPID  STAT  COMMAND
2847  1234  Z     [bash] &amp;lt;defunct&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Kernel Never Forgets
&lt;/h2&gt;

&lt;p&gt;When a child process calls &lt;code&gt;exit()&lt;/code&gt;, the Kernel does the cleanup you'd expect — frees the memory, closes the file descriptors, tears down the execution context. But it deliberately keeps one thing alive: &lt;strong&gt;a small entry in the process table&lt;/strong&gt; containing the exit status and the PID.&lt;/p&gt;

&lt;p&gt;Why? Because the Kernel assumes the parent might want to know how the child died. Was it successful? Did it crash? What was the exit code? The Kernel holds onto that answer and waits for the parent to come collect it — a system call called &lt;code&gt;waitpid()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Until the parent collects it, the child is a Zombie. Not running. Not consuming memory or CPU. Just a row in a table, waiting to be acknowledged.&lt;/p&gt;

&lt;p&gt;This is by design. The problem is when the parent never comes to collect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Zombie Is Born in Java
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="c1"&gt;// We consumed the stream — good.&lt;/span&gt;
&lt;span class="c1"&gt;// But we never called waitFor() or onExit().&lt;/span&gt;
&lt;span class="c1"&gt;// The process finishes, the Kernel holds the exit status,&lt;/span&gt;
&lt;span class="c1"&gt;// and the zombie sits in the process table indefinitely.&lt;/span&gt;
&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"echo 'done'; exit 0"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// process.waitFor() or onExit() — neither was called.&lt;/span&gt;
&lt;span class="c1"&gt;// The parent never collected the death certificate.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child is dead. The output was drained. But the Kernel is still holding a row in the process table, waiting for your Java app to call &lt;code&gt;waitpid()&lt;/code&gt; and acknowledge the exit. Your Java app never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Always Acknowledge the Exit
&lt;/h2&gt;

&lt;p&gt;The fix is simple. Always give the process a way to have its exit status collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking — &lt;code&gt;waitFor()&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"echo 'done'; exit 0"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// This is what collects the death certificate.&lt;/span&gt;
&lt;span class="c1"&gt;// The Kernel hands the exit status to Java and removes the process table entry.&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exited with: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Non-blocking — &lt;code&gt;onExit()&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onExit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenCompose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;drainTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenApply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PID "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" exited with: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exitValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both patterns do the same thing at the OS level — they trigger &lt;code&gt;waitpid()&lt;/code&gt;, hand the exit status to your Java code, and allow the Kernel to finally clear that process table entry. No zombie.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The JVM actually has a background reaper thread that calls &lt;code&gt;waitpid()&lt;/code&gt; internally to clean up child processes, so you won't see zombies under normal conditions. But if you're spawning thousands of short-lived processes per second, the reaper can fall behind. Zombies accumulate faster than they're collected. PIDs are a finite resource on Linux — exhaust them, and your entire system stops being able to create new processes. Not just your JVM. Everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One More Thing: Timeouts
&lt;/h2&gt;

&lt;p&gt;What if the process never exits? A hung subprocess, a command waiting on input that never comes, a network call that stalls. Your &lt;code&gt;waitFor()&lt;/code&gt; blocks forever. Your &lt;code&gt;onExit()&lt;/code&gt; never fires.&lt;/p&gt;

&lt;p&gt;Always give long-running processes a deadline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;finished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SIGTERM first - polite, gives the process a chance to clean up&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;destroy&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// If still alive after grace period, SIGKILL - no arguments&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;destroyForcibly&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Process timed out and was killed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;destroy()&lt;/code&gt; sends SIGTERM — a polite request to shut down. The process can catch this and clean up gracefully. &lt;code&gt;destroyForcibly()&lt;/code&gt; sends SIGKILL — the Kernel tears it down immediately, no questions asked. Always try SIGTERM first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A zombie isn't scary by itself. It's a design feature of the OS that assumes you'll come collect the exit status. The danger is volume and neglect — thousands of zombies exhausting your PID space, or a deadlocked parent that can never collect anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The rule is simple: every process you spawn must have either &lt;code&gt;waitFor()&lt;/code&gt; or &lt;code&gt;onExit()&lt;/code&gt; attached to it. No exceptions.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  File Descriptor Leakage: Exhausting the Doors
&lt;/h1&gt;

&lt;p&gt;Everything was fine. Your app was running, processes were spawning, output was being consumed. Then one morning, this shows up in your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.io.IOException: Too many open files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not out of memory. Not a NullPointerException. The JVM can't open a log file. Can't accept a new network connection. Can't spawn another process. Your entire application is crippled.&lt;/p&gt;

&lt;p&gt;There are two ways to get here. One is obvious. The other will surprise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Obvious One: Not Closing Your Streams
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;pb.start()&lt;/code&gt;, the Kernel creates pipes between your Java process and the child. Three FDs, every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FD → child's STDIN  (write-end your Java code holds)
FD → child's STDOUT (read-end your Java code holds)
FD → child's STDERR (read-end your Java code holds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you read from those streams but never explicitly close them, those FDs stay open. The JVM will &lt;em&gt;eventually&lt;/em&gt; close them during GC finalization — but finalization is non-deterministic. On a busy server spawning processes in a loop, you'll hit the ceiling long before GC gets around to it.&lt;/p&gt;

&lt;p&gt;The fix is mechanical: &lt;code&gt;try-with-resources&lt;/code&gt; on every stream, every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BufferedReader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InputStreamReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="o"&gt;())))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the obvious case. Most developers learn it once, fix it, and move on.&lt;/p&gt;

&lt;p&gt;The second case is harder — because the code is correct and it still breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Surprising One: Correct Code That Still Exhausts FDs
&lt;/h2&gt;

&lt;p&gt;Look at this. Streams are discarded at the OS level with &lt;code&gt;Redirect.DISCARD&lt;/code&gt;. &lt;code&gt;onExit()&lt;/code&gt; is wired up. No streams held open anywhere. By every measure, this is correct ProcessBuilder code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sleep"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"100"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redirectOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Redirect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DISCARD&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;processList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;processList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPAWNING Process: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onExit&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EXIT CODE: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exitValue&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FD LIMIT HIT****"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;processList&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;destroyForcibly&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see this break without waiting for a production disaster, launch JShell with a hard FD ceiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'ulimit -n 64 &amp;amp;&amp;amp; jshell'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can verify the ceiling is applied to your process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&amp;lt;PID&amp;gt;/limits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5b4wue1gjz8zm1opl3pu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5b4wue1gjz8zm1opl3pu.png" alt="PID Limits"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at that. Soft limit and hard limit both set to 64. Every FD the process holds counts against that number.&lt;/p&gt;

&lt;p&gt;Now run the code. Here's what happens:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32sxj40ia2eegvrobkkt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32sxj40ia2eegvrobkkt.png" alt="PID Limits of Process"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No stream leak. No missing &lt;code&gt;try-with-resources&lt;/code&gt;. The code is doing everything right — and it still hits the wall at process 23.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Actually Happening
&lt;/h2&gt;

&lt;p&gt;Notice the error message carefully. It's not complaining about a stream. It's complaining about the &lt;strong&gt;spawn helper&lt;/strong&gt; — the internal JVM mechanism used to fork and exec new processes. That machinery needs FDs too. When the OS ceiling is hit, even the act of spawning a new process fails.&lt;/p&gt;

&lt;p&gt;Now look at what &lt;code&gt;sleep 100&lt;/code&gt; means. Each process is alive for 100 seconds. Each alive process holds OS-level resources — not your stream FDs, but the process entry itself and the JVM's internal handles for managing it. With a ceiling of 64 and processes accumulating faster than they exit, you run out around process 21. The math is ruthless.&lt;/p&gt;

&lt;p&gt;This is the distinction that matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FD leak&lt;/strong&gt; — streams opened, never closed. Processes exit but Java keeps holding their pipes. Fix: &lt;code&gt;try-with-resources&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FD exhaustion&lt;/strong&gt; — streams handled correctly. But too many processes alive simultaneously, each consuming OS resources, faster than the ceiling allows. Fix: be conscious of concurrency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;try-with-resources&lt;/code&gt; solves the first problem completely. It does nothing about the second. You can write textbook-correct ProcessBuilder code and still bring down a production server if you're spawning long-lived processes in an unbounded loop.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FD exhaustion is a slow poison. It doesn't crash your app immediately. It builds quietly — each spawned process holding OS resources — until the Kernel says enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two habits keep you safe. First: &lt;code&gt;try-with-resources&lt;/code&gt; on every stream, every time. Second: stay conscious of how many processes are alive simultaneously. Correct per-process code isn't enough if you're spawning without bounds.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The exitValue() Race Condition: Never Ask a Running Process How It Died
&lt;/h1&gt;

&lt;p&gt;This one is short, sharp, and easy to get wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;exitValue()&lt;/code&gt; returns the exit code of a process. Simple enough. The problem is that it has a precondition nobody warns you about — &lt;strong&gt;the process must already be finished&lt;/strong&gt;. Call it on a process that's still running and it doesn't block, it doesn't wait, it doesn't retry. It throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.lang.IllegalThreadStateException: process has not exited
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a race condition in its purest form. Your code assumes the process is done. The OS disagrees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where People Get Caught
&lt;/h2&gt;

&lt;p&gt;The tempting pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onExit&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handling exit here...&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Meanwhile, somewhere else in the code...&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exitValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// process might still be running - boom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the even subtler version — calling &lt;code&gt;exitValue()&lt;/code&gt; right after &lt;code&gt;start()&lt;/code&gt; assuming a fast command finishes instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exitValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// might work locally, blows up in production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It might work in development. The command is fast, the process exits before the next line runs, and you never see the exception. Then in production, under load, with a slower machine or a busier OS scheduler, that assumption breaks. The process hasn't exited yet. The exception hits. And it only happens sometimes — which makes it the worst kind of bug to track down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: You Already Have It
&lt;/h2&gt;

&lt;p&gt;You don't need anything new here. The patterns we've already covered handle this correctly by design.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;waitFor()&lt;/code&gt; blocks until the process exits and returns the exit code — safe by definition. The process is guaranteed finished before you ever touch the code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onExit()&lt;/code&gt; fires only after the process has terminated — &lt;code&gt;exitValue()&lt;/code&gt; inside the callback is always safe because the OS has already confirmed the process is dead before the callback runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule is simple: &lt;strong&gt;never call &lt;code&gt;exitValue()&lt;/code&gt; outside of &lt;code&gt;waitFor()&lt;/code&gt; or an &lt;code&gt;onExit()&lt;/code&gt; callback&lt;/strong&gt;. There is no legitimate reason to call it raw. If you find yourself reaching for it directly, that's a signal something is wrong with the surrounding logic — not a clever optimization.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;exitValue()&lt;/code&gt; isn't broken — it's just honest. It tells you the exit code of a finished process. Call it on an unfinished one and it refuses. The fix isn't a workaround — it's just using &lt;code&gt;waitFor()&lt;/code&gt; or &lt;code&gt;onExit()&lt;/code&gt; the way they were designed, which you're already doing if you've followed everything up to this point.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Environment Pollution: Don't Give Your Children What They Don't Need
&lt;/h1&gt;

&lt;p&gt;There's one more thing worth getting right before you ship any ProcessBuilder code to production.&lt;/p&gt;

&lt;p&gt;By default, every child process you spawn inherits the &lt;strong&gt;entire environment&lt;/strong&gt; of your Java process. Every environment variable your JVM was started with — all of it gets copied to the child.&lt;/p&gt;

&lt;p&gt;That includes everything — database URLs, API keys, AWS credentials, internal service tokens. Things you never intended to hand to a shell command or a third-party binary. You didn't make a mistake. You just didn't think about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Clear and Inject Only What's Needed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt; exposes the child's environment as a plain &lt;code&gt;Map&lt;/code&gt;. Clear it, then put back only what the command actually needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"echo $MY_VAR"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// wipe everything the JVM inherited&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MY_VAR"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"only_what_is_needed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// give the child exactly this, nothing more&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The child process should know exactly what it needs and nothing more. Clear the environment. Be deliberate. A clean environment isn't just a security practice — it's also what makes child processes predictable. No accidentally inherited &lt;code&gt;JAVA_OPTS&lt;/code&gt;, no conflicting &lt;code&gt;PATH&lt;/code&gt;, no debugging a binary that's misbehaving because of something the parent leaked in.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Respect the Plumbing
&lt;/h1&gt;

&lt;p&gt;Stepping outside the JVM isn't a trivial thing. The moment you call &lt;code&gt;pb.start()&lt;/code&gt;, you are no longer in managed territory. The JVM cannot save you from a clogged pipe, a zombie process, a leaked file descriptor, or a race condition on an exit code. That responsibility lands entirely on you.&lt;/p&gt;

&lt;p&gt;But here's the thing — none of it is complicated once you understand what the OS is actually doing underneath. Every problem we covered in this article traces back to one root cause: &lt;strong&gt;treating &lt;code&gt;pb.start()&lt;/code&gt; like a Java method call when it's really an OS operation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the complete playbook in one place:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Drain the pipe before you wait:&lt;/strong&gt; The 64KB buffer fills up and the OS freezes your child process. Always give the output somewhere to go — a reader thread, a file, the terminal — before calling &lt;code&gt;waitFor()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always collect the exit status:&lt;/strong&gt; Every process you spawn must have either &lt;code&gt;waitFor()&lt;/code&gt; or &lt;code&gt;onExit()&lt;/code&gt; attached to it. No exceptions. The Kernel is holding that exit status until you come collect it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Close your streams explicitly:&lt;/strong&gt; Don't trust GC with OS resources. &lt;code&gt;try-with-resources&lt;/code&gt; on every stream, every time. And stay conscious of how many processes are alive simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never call &lt;code&gt;exitValue()&lt;/code&gt; raw:&lt;/strong&gt; Only inside &lt;code&gt;waitFor()&lt;/code&gt; or an &lt;code&gt;onExit()&lt;/code&gt; callback. Everywhere else is a race condition waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanitize the environment:&lt;/strong&gt; Clear &lt;code&gt;pb.environment()&lt;/code&gt; and inject only what the child process actually needs. Your secrets are not its business.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Get these things right, and ProcessBuilder stops being a source of 3 AM incidents. It becomes exactly what it's supposed to be — a clean, powerful bridge between your Java code and the operating system it runs on.&lt;/p&gt;

</description>
      <category>java</category>
      <category>linux</category>
      <category>backend</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
