memory_get_usage() 持续上涨但脚本结束rss不回落,说明泄漏源在php层外:c扩展内存、静态缓存、gd/redis资源未显式销毁,或循环引用未破除;需结合gc_collect_cycles()、debug_zval_dump()和ps aux rss监控分层排查。

memory_get_usage() 能看到内存持续上涨,但脚本结束时 RSS 没回落?那基本不是 PHP 自身 GC 的问题,而是你漏掉了某些“不自动释放”的资源或引用。
为什么 unset() 后内存没降下来
PHP 的 unset() 只销毁变量名和其对值的引用,不等于立即归还内存给系统。尤其当值被其他变量、静态属性、全局数组或扩展级缓存强引用时,内存仍被占用。
常见陷阱:
-
unset($obj)无法释放循环引用中的对象(如DOMDocument和子节点互持引用) - 静态属性缓存未清理:
self::$cache[$key] = new HeavyObject();后不主动清空,生命周期贯穿整个请求甚至进程 - GD 图像资源未调用
imagedestroy(),底层 C 内存不会被 Zend MM 管理 - 扩展中混用
malloc()和efree(),导致 Zend 内存管理器跳过释放
如何识别真正的泄漏点
靠 memory_get_usage() 只能看到 PHP 层内存,掩盖了 C 扩展或系统级泄漏。关键要分层验证:
- 用
gc_collect_cycles()返回值判断是否真有循环引用残留(返回 >0 才说明 GC 清理了) - 在 CLI 模式下启用
ZEND_MM_DEBUG=1运行脚本,观察emalloc/efree是否配对 - 对可疑对象用
debug_zval_dump($var)查看refcount__gc和is_ref__gc,确认是否还有外部强引用 - Swoole 常驻进程必须配合
max_requests重启兜底,但不能替代主动释放
哪些资源必须显式销毁
PHP 不会自动回收三类资源:C 扩展分配的内存、双向绑定的对象图、进程级静态缓存。它们必须手动断链或销毁:
- GD 图像:
imagedestroy($image)—— 忘记它,10MB 图片会多占 10MB RSS - XML/DOM:
$dom->clear()或$dom->__destruct()(部分版本需显式触发) - 旧版 PhpSpreadsheet:
$spreadsheet->disconnectWorksheets(),否则 worksheet ↔ workbook 引用锁死内存 - 自定义缓存容器:用
WeakMap替代普通数组,键对象被回收时条目自动消失 - 数据库长连接或流句柄:显式
$pdo = null或fclose($fp)
静态缓存和 Swoole 场景下的特殊处理
Web SAPI(如 FPM)每个请求后重置符号表,但 static、global、扩展级静态指针不会清空——这是常驻泄漏的重灾区。
典型错误模式:
- 类内
private static $instances = [];缓存大型对象,无配套clearAll()方法 - Swoole 中把 Redis 客户端或配置数据塞进
static,随 worker 寿命无限累积 - 扩展中用
static zend_string *cache_key存字符串,却没在 RSHUTDOWN 阶段zend_string_release()
安全做法:
- 所有静态缓存必须提供清理入口,并在
register_shutdown_function()或扩展的PHP_RSHUTDOWN_FUNCTION中调用 - Swoole worker 设置
max_request = 1000是底线,不是解决方案 - 优先用
WeakMap(PHP 8.0+)替代array做对象级缓存
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











