Runtime.exec卡住的主因是未及时读取stdout/stderr导致缓冲区满而阻塞,且exec(String)会错误拆分含空格路径;ProcessBuilder虽改进参数、目录和环境变量支持,但默认不合并错误流,需显式调用redirectErrorStream(true)。

Runtime.exec 为什么经常卡住或得不到输出
根本原因不是命令写错了,而是 Runtime.exec 启动的进程默认把 stdout 和 stderr 当成管道,不主动读取就会阻塞(尤其输出量大时)。很多代码只调用 waitFor(),没处理流,结果程序挂住不动。
- 必须用单独线程或循环及时消费
process.getInputStream()和process.getErrorStream(),否则子进程可能因缓冲区满而停在 write 阶段 -
Runtime.exec(String)会按空格拆分参数,遇到含空格路径(如"C:\Program Files\java\bin\java.exe")直接失败;要用Runtime.exec(String[])显式传参数组 - 环境变量和工作目录无法设置——
Runtime.exec总是继承当前 JVM 的环境,不能指定cwd
ProcessBuilder 更安全但默认不合并错误流
ProcessBuilder 是为解决 Runtime.exec 的硬伤设计的:支持设置工作目录、环境变量、自动处理参数空格。但它有个反直觉行为:stderr 默认不重定向到 stdout,所以你用 getInputStream() 读不到报错信息。
- 用
redirectErrorStream(true)才能让错误输出混入标准输出流,方便统一读取 - 启动前务必调用
directory(File)明确工作路径,否则依赖当前 JVM 目录,CI/容器中极易出错 - 传参推荐用
new ProcessBuilder("ping", "-c", "1", "localhost")这种列表形式,避免 shell 解析歧义 - Windows 下若需执行
dir或echo等内置命令,得显式调用cmd /c dir,ProcessBuilder不走 shell
捕获输出时别直接 toString() InputStream
常见错误是把 process.getInputStream() 转成 byte[] 再 new String,这既忽略字符编码,又可能因流未关闭导致读不全。JVM 默认编码不等于系统 locale,中文路径或输出大概率乱码。
- 用
new InputStreamReader(stream, StandardCharsets.UTF_8)显式指定编码(Linux/macOS 基本 OK,Windows 命令行常用GBK或MS932) - 别用
available()判断流是否结束——它只返回当前可读字节数,非 EOF,循环读到read()返回 -1 才算完 - 实际项目中建议用
Files.readAllBytes(redirectedOutput.toPath())配合redirectOutput(File),绕过流管理复杂度
要不要用 Apache Commons Exec 或其他封装库
如果只是偶尔执行简单命令(如 git status),原生 ProcessBuilder 加上流处理足够。但一旦涉及超时控制、信号中断、实时日志回调、跨平台 shell 兼容(比如 Windows 的 && vs Linux 的 ;),自己写容易漏边界。
立即学习“Java免费学习笔记(深入)”;
-
Apache Commons Exec提供DefaultExecutor和PumpStreamHandler,能自动处理流、设超时、支持回调,比手撸健壮得多 - 注意它默认仍不合并错误流,要手动
setExitValue(0)并配置PumpStreamHandler的stderr输出目标 - 新项目可考虑
io.github.cdimascio:dotenv-java这类轻量工具链,但别为一行ls引入整个 exec 生态
真正难的从来不是“怎么启动进程”,而是“怎么确保它一定退出、输出不丢、错误可定位、超时能杀”。这些细节藏在流读取顺序、编码声明、工作目录隔离里,而不是 API 选哪个。









