必须显式关闭Runtime.exec或ProcessBuilder启动进程的输入、输出、错误流,否则因缓冲区满导致子进程阻塞、死锁,并引发操作系统资源泄露和JVM本地内存持续增长。

这种情况本质是子进程资源未释放引发的内存泄漏,不是单纯“句柄耗尽”,而是子进程持续驻留、堆内存与内核资源双重累积所致。Java中 Runtime.exec() 或 ProcessBuilder.start() 启动的进程,若未显式调用 destroy() 或 destroyForcibly(),且未消费其输入/输出流,会导致子进程僵死、JVM无法回收关联的本地内存结构,最终拖垮整个系统。
确认子进程是否真实残留
不要只看 Java 进程本身,重点查它 spawn 出来的外部程序:
- 用
ps aux | grep sst(替换成你实际调用的命令,如/sstweb/sst)确认该进程是否仍在运行,尤其注意其父进程 PID 是否为你的 Java 进程 PID - 检查 Java 进程的子进程树:
ps --ppid <java-pid> -o pid,ppid,comm,args,看是否有未退出的子进程挂在其下 - 观察
/proc/<java-pid>/fd/目录:执行ls -l /proc/<java-pid>/fd/ | grep socket\|pipe,大量未关闭的 pipe(类型为pipe或anon_inode:pipe)是典型征兆——说明子进程的 stdin/stdout/stderr 流仍被持有
定位代码中缺失 destroy() 的调用点
常见遗漏场景集中在异常分支和异步逻辑中:
- 仅在正常流程调用
process.waitFor()后destroy(),但 catch 块里直接 return 或 throw,跳过了清理 - 使用
Future<Process>或线程池提交任务,未在 finally 或 CompletionHandler 中确保destroyForcibly() - 调用
process.getInputStream().readAllBytes()等阻塞方法时发生超时或中断,后续未兜底销毁 - 忘记对
Process对象做 null 判空(某些 exec 失败可能返回 null,后续调用 destroy 会 NPE,掩盖真正问题)
验证 JVM 本地内存是否已堆积
子进程泄漏会显著推高 JVM 的“内部本地内存”(Native Memory),而非堆内存:
- 启用本机内存追踪:
-XX:NativeMemoryTracking=detail启动 JVM,再用jcmd <pid> VM.native_memory summary查看Internal和Thread区域是否异常增长 - 对比
top -p <pid>中的 RES(物理内存)与jstat -gc <pid>显示的堆内存,若 RES 持续上涨而堆稳定,基本可锁定为本地资源泄漏 - 用
pstack <pid>查看线程栈,若大量线程卡在java.lang.UNIXProcess.waitForProcessExit或java.io.FileInputStream.readBytes,说明正阻塞在子进程 I/O 上
修复与防护措施
不能只靠事后 destroy(),必须构建防御性调用链:
- 始终用 try-with-resources 包裹
Process(Java 9+ 支持AutoCloseable),或手动在 finally 块中调用destroyForcibly()(比destroy()更可靠) - 对子进程流必须消费:哪怕只是
process.getInputStream().transferTo(OutputStream.nullOutputStream()),避免管道缓冲区填满导致子进程挂起 - 设置超时:用
process.waitFor(30, TimeUnit.SECONDS)替代无参阻塞调用;超时后立即destroyForcibly() - 上线前加监控:定期采集
ps --ppid <java-pid> | wc -l,子进程数 > 5 即告警;或用cat /proc/<pid>/status | grep Threads监控线程数突增

















