Apache Prefork 模式本身不产生僵尸进程,根源在于Java应用通过Runtime.exec()或ProcessBuilder启动子进程后未正确wait回收;需在Java侧显式调用waitFor()、消费IO流并设置超时,结合Apache MaxRequestsPerChild限制worker寿命来防控。
apache 在 prefork mpm 模式下本身不会直接产生僵尸进程,但若其子进程(worker 进程)在运行中 fork 出子进程且未正确回收,而父进程又未调用 wait() 或 waitpid(),就可能遗留僵尸进程。java 应用通常通过 cgi、servlet 容器(如 tomcat)或反向代理方式与 apache 协作,真正产生僵尸进程的源头往往是 java 进程内部的本地调用(如 runtime.exec() 或 processbuilder 启动外部程序)未妥善处理子进程生命周期。
为什么 Prefork 模式下容易观察到僵尸进程?
Prefork 模式使用多个长期存活的、预派生的子进程(每个子进程单线程)来处理请求。这些子进程生命周期长(默认可服务数千请求),若其中某个子进程在处理请求时调用 Java 代码(例如通过 mod_jk/mod_proxy_ajp 连接 Tomcat,或通过 exec 调用 Java CLI 工具),而该 Java 代码又启动了子进程(如 shell 命令、Python 脚本、FFmpeg 等),就可能出现以下情况:
- Java 使用
ProcessBuilder启动进程后,未调用process.waitFor()或未消费process.getInputStream()/getErrorStream(),导致子进程退出后其退出状态未被父进程读取; - JVM 未设置
destroyOnExit(false),且未显式调用destroy()+waitFor()组合清理; - Java 进程异常终止(如 OOM、SIGKILL),来不及执行 finally 块中的子进程回收逻辑;
- Apache 子进程因配置(如
MaxRequestsPerChild)重启时,已 fork 但尚未 wait 的子进程变成孤儿,被 init(PID 1)收养,但 init 若未及时 wait,就会短暂成为僵尸。
如何定位僵尸进程是否来自 Apache-Java 协同链路?
执行以下命令快速确认:
-
ps aux | grep 'Z' | grep -v grep查看僵尸进程(STAT 列为 Z); -
ps -o pid,ppid,comm,state -C java查看 Java 进程及其父进程 PID(PPID); -
ps -o pid,ppid,comm,state -p <apache_worker_pid>检查对应 Apache worker 是否是僵尸进程的父进程; - 查看
/proc/<pid>/stack和/proc/<pid>/status,确认PPid:及State: Z,并结合TracerPid:判断是否被调试器阻塞; - 启用 Apache
LogLevel debug并开启mod_status,观察 worker 进程启停频率是否与僵尸出现时间吻合。
Java 侧关键修复措施
核心原则:**每个 Process 对象必须被显式等待和释放**。推荐写法:
- 始终使用 try-with-resources 包裹
Process(JDK 9+ 支持),或手动确保waitFor()执行; - 务必消费子进程的输入/错误流(哪怕只丢弃),防止缓冲区阻塞导致子进程挂起(进而无法正常退出);
- 避免仅调用
destroy()—— 它不等待退出,应配合waitFor(); - 对关键外部调用添加超时机制:
process.waitFor(30, TimeUnit.SECONDS),超时后强制 destroy 并记录告警; - 若需频繁执行外部命令,改用线程池 + Apache Commons Exec,它内置流自动处理和超时支持。
Apache 与系统层辅助防护
从运行环境降低风险:
立即学习“Java免费学习笔记(深入)”;
- 设置
MaxRequestsPerChild为合理值(如 5000),避免 worker 进程驻留过久,减少长期累积子进程的风险; - 在 Apache 启动脚本中添加
echo 1 > /proc/sys/kernel/panic_on_oops(可选),提升内核对异常的响应; - 配置 systemd(如使用
systemd-notify)或 cron 定期清理:ps aux | awk '$8 ~ /^Z$/ { print $2 }' | xargs kill -SIGCHLD 2>/dev/null(注意:仅对 init 收养的僵尸有效,且需 root); - 监控指标接入:采集
/proc/stat中processes和procs_blocked,结合ps统计僵尸数,触发告警。
不复杂但容易忽略:僵尸本身不消耗 CPU 或内存,但会占用进程表项;持续积累可能耗尽 PID 空间,导致新进程无法创建。问题根因几乎总在 Java 代码的子进程管理逻辑,而非 Apache Prefork 模式本身。










