本文目录导读:

在 PHP 中实现“内存常驻”(即进程持续运行,不释放内存,常驻内存)通常有几种方法,最核心的思想是让 PHP 进程不结束,从而让变量、连接和缓存数据一直保留在内存中。
根据你的需求(是写脚本、做队列消费,还是开发高性能服务),主要有以下三种主流方案:
使用 while(true) 循环(原生 CLI 方式)
这是最基础、最直接的方法,PHP 执行完代码后,如果进程不退出,内存自然就常驻了。
适用场景: 简单的定时任务、单进程队列消费、简单的长连接脚本。
核心代码示例(一个简单的消费者):
<?php
// 在 CLI 模式下运行: php consumer.php
// 关闭 PHP 默认的执行时间限制(否则脚本会自动退出)
set_time_limit(0);
// 关闭输出缓冲,让日志实时输出
ob_implicit_flush(true);
// 模拟一个全局缓存或数据库连接
$cache = [];
echo "Worker 已启动,内存常驻中...\n";
while (true) {
// 模拟从队列(如 Redis/DB)拉取任务
$task = getTaskFromQueue(); // 假设这个函数会阻塞或者等待
if ($task !== null) {
echo "处理任务: " . $task . "\n";
// 将结果存入内存,实现“记忆”
$cache[$task] = time();
}
// **重要:防止 CPU 空转 100%**
usleep(100000); // 休眠 0.1 秒再继续
}
// 下面的代码永远不会执行到
关键点:
- 在
while循环内,变量$cache一直存在且可读写。 - 必须要在循环里加
usleep()或sleep(),否则 CPU 会飙升。
缺点: 代码结构简单,但如果循环内发生未捕获的异常,进程会直接退出,需要很严谨的异常处理来保活。
使用 pcntl_fork() 多进程(CLI)
如果你需要同时处理多个任务,且希望主进程常驻挂载子进程,可以用进程控制函数。
适用场景: 复杂的并发队列消费(如多开的 Worker)。
核心思路:
<?php
// 主进程(Supervisor)
set_time_limit(0);
// 创建 3 个子进程
for ($i = 0; $i < 3; $i++) {
$pid = pcntl_fork();
if ($pid == -1) {
die('无法创建子进程');
} elseif ($pid) {
// 这是父进程
echo "已启动子进程 PID: $pid\n";
} else {
// 这是子进程
while (true) {
echo "子进程 " . getmypid() . " 处理中...\n";
sleep(1);
// 这里可以放具体的业务逻辑
}
exit(); // 子进程退出,不会影响父进程
}
}
// 父进程进入监控状态,等待信号回收子进程
while (true) {
$status = 0;
$pid = pcntl_waitpid(-1, $status, WNOHANG); // 非阻塞等待子进程退出
if ($pid > 0) {
echo "子进程 $pid 退出了,主进程继续运行。\n";
// 这里可以重新 fork 新进程补充
}
sleep(1);
}
使用 Swoole / Workerman(生产环境首选)
如果你是需要高性能 HTTP 服务、WebSocket 或复杂的 TCP 服务,强烈推荐使用 Swoole 扩展或 Workerman 框架,它们本质上是 C 语言写的事件循环 + PHP 业务逻辑,内存管理极其高效,且提供了完善的进程管理机制。
适用场景: 高并发 API 服务、长连接推送、微服务。
Swoole 核心示例(内存常驻 + 热更新):
<?php
// 文件: server.php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
// 创建 HTTP 服务,监听 9501 端口
$server = new Server("0.0.0.0", 9501);
// 设置参数
$server->set([
'worker_num' => 4, // 开启 4 个 Worker 进程
'daemonize' => false, // 前台运行 (方便调试)
'max_request' => 10000, // 每个 Worker 处理 1 万请求后重启,防止内存溢出
]);
// 这段代码在 Worker 进程启动时只执行一次,内存常驻
$server->on('WorkerStart', function ($server, $workerId) {
// 在这里初始化全局数据库连接或共享数据(仅限该 Worker 内)
global $db;
$db = new PDO('mysql:host=...', 'user', 'pass');
echo "Worker $workerId 启动,内存常驻!\n";
});
// 回调函数
$server->on('request', function (Request $request, Response $response) {
// 这里面的局部变量 $request 每次请求都会重新创建
// WorkerStart 中的 $db 连接是常驻的
$response->header("Content-Type", "text/plain");
$response->end("Hello World - 这是常驻进程!\n");
});
echo "服务器启动,PID: " . $server->master_pid . "\n";
$server->start();
关键注意事项(避坑指南)
-
内存泄漏(Memory Leak):
- 虽然建议常驻,但 PHP 本质是脚本语言,没有不引用的对象自动 GC,如果你在循环里一直
new对象而不释放,内存会缓慢增加。 - 解决方法:使用
unset()释放大变量;如果是 Swoole,设置max_request让 Worker 定期自动重启,是防止内存泄漏的标配操作。
- 虽然建议常驻,但 PHP 本质是脚本语言,没有不引用的对象自动 GC,如果你在循环里一直
-
set_time_limit(0):- 在 CLI 模式下,默认
max_execution_time为 0(无限),但如果是在某些框架中,可能需要手动设置ini_set('max_execution_time', '0')。
- 在 CLI 模式下,默认
-
异常与崩溃恢复:
- 常驻进程最怕
Fatal Error(如内存耗尽),用register_shutdown_function或try...catch捕获,或者用pcntl_signal监听退出信号做平滑退出。
- 常驻进程最怕
-
代码修改后如何生效:
- 常驻进程启动后,修改 PHP 文件不会生效(因为代码已经加载进内存了)。
- 必须重启进程,如果是 Swoole,可以用
SIGUSR1信号实现平滑重载(Reload)。
总结建议
- 如果只是临时测试:用
while(true)+usleep(0.1)最方便。 - 如果是做正式项目/服务:强烈建议直接用 Swoole 或 Workerman,它们帮你处理好了进程管理、信号、内存回收等底层复杂逻辑,你只需要专注写业务代码即可。
如果你在 Windows 下开发,Swoole 不支持 Windows,可以考虑使用 Workerman(纯 PHP 实现)或者用 Docker/Linux 环境。