PHP内存不释放主因是循环引用、全局/静态变量残留、扩展资源未显式销毁及C层内存管理错配;需用WeakMap、显式断链、gc_collect_cycles、ZEND_MM_DEBUG等定位并修复。

memory_get_usage() 能看到增长,但脚本结束后 RSS 没回落?那基本不是 PHP 自身 GC 的问题,而是你漏掉了某些“不自动释放”的资源或引用。
PHP 内存泄漏最常发生在循环引用和对象未显式断链时
PHP 7.4+ 的垃圾回收器能处理大多数循环引用,但前提是:对象必须真正脱离作用域,且没有外部强引用残留。像 PHPExcel、DOMDocument、自定义缓存容器这类组件,内部大量使用双向引用(如 worksheet ↔ workbook、node ↔ document),unset() 只销毁变量名,不打破对象图里的指针。
常见现象:
- 循环中创建对象后
unset($obj),但内存仍持续上涨 -
gc_collect_cycles()返回 0,却仍有未释放对象 - 用
valgrind --tool=memcheck或ZEND_MM_DEBUG=1启动 PHP,发现大量emalloc分配未被efree匹配
实操建议:
- 对已知有循环结构的库(如旧版 PhpSpreadsheet),调用其提供的显式断开方法:
$spreadsheet->disconnectWorksheets() - 避免在函数内直接
new大型对象并返回,改用工厂 + 显式destroy()接口 - 用
debug_zval_dump($var)查看refcount__gc和is_ref__gc,确认是否真已无强引用
全局/静态变量是常驻泄漏的重灾区
Web SAPI(如 Apache mod_php、FPM)里,每个请求结束会重置符号表,但全局变量、static 属性、扩展级静态指针(如 C 扩展里的 static zend_string *cache_key)不会自动清空——尤其在 Swoole 常驻进程中,它们会累积整个进程生命周期。
立即学习“PHP免费学习笔记(深入)”;
典型错误写法:
class CacheManager {
private static $instances = [];
public static function get($key) {
if (!isset(self::$instances[$key])) {
self::$instances[$key] = new HeavyObject(); // emalloc 分配
}
return self::$instances[$key];
}
}
实操建议:
- 所有静态缓存必须配套清理逻辑:在
PHP_RSHUTDOWN_FUNCTION(扩展)或register_shutdown_function()(PHP 层)中遍历释放 - 用
WeakMap替代普通数组缓存:private static $cache = new WeakMap();—— 键对象被回收时,对应条目自动消失 - Swoole 场景下,严格限制
max_request(如设为 1000),靠进程重启兜底,但不能替代主动释放
扩展开发中 emalloc/efree 不配对等于直接泄漏
PHP 扩展若混用 malloc 和 efree,或在析构器里漏掉 zend_string_release(),就会触发底层内存管理器跳过释放——因为 efree 只认 Zend Memory Manager 分配的地址头。
错误示例:
// 错误:zval 字符串值由 emalloc 分配,但用 free 释放 zval *zv; ZVAL_STRING(zv, "hello"); free(Z_STRVAL_P(zv)); // ❌ 漏掉 Z_STR_P(zv)->val 的 refcount,且 free ≠ efree
实操建议:
- 所有
emalloc/ecalloc/erealloc必须配efree;所有malloc/calloc配free - 操作
zval字符串字段前,先检查分配来源:ZSTR_IS_INTERNED(Z_STR_P(zv))判定是否需zend_string_release - 启用
ZEND_MM_DEBUG=1运行,看请求结束时是否输出 “Leaks detected” 及堆栈
图像、XML、数据库连接等资源不 destroy 就不释放
GD、libxml、PDO 等扩展封装的资源(resource 类型)不调用对应释放函数,其底层 C 结构体和缓冲区会长期驻留。PHP 的资源计数器只管 resource 句柄,不管背后内存。
典型遗漏点:
-
imagecreatefromjpeg()后没跟imagedestroy() -
simplexml_load_file()返回对象后,没调用simplexml_import_dom()或unset()清理 DOM 引用 - PDOStatement 执行完大结果集后,没调用
$stmt->closeCursor()释放结果集内存
实操建议:
- 资源创建和销毁尽量成对出现在同一作用域,用
try/finally包裹 - 对 GD 图像,统一用
gdImagePtr类型封装,析构时强制imagedestroy - 用
get_resources('gd')或get_resources('mysql link')在关键点检查残留资源数



















