内存泄漏检测
—–
PHP 自带泄漏检测(最简单):
# 编译时开启
./configure —enable–debug
# 运行时自动报告泄漏
php –r "
\\$arr = [];
for (\\$i = 0; \\$i < 100; \\$i++) {
\\$arr[] = str_repeat('x', 1024);
}
// 故意不释放
"
# 进程退出时输出
# [Mon Jan 1 00:00:00 2024] Script: '-'
# /path/to/zend_alloc.c(123) : 4096 bytes leaked
—–
Valgrind(最全面):
# 编译 PHP 时关掉内存管理器,让 Valgrind 接管
./configure —enable–debug —enable–valgrind–checks
# 运行
valgrind \\
—leak–check=full \\
—show–leak–kinds=all \\
—track–origins=yes \\
—suppressions=valgrind.supp \\ # 忽略PHP自身的"假泄漏"
php your_script.php 2>&1 | tee leak.log
# 输出
# ==1234== 1,024 bytes in 1 blocks are definitely lost
# ==1234== at 0x4C2FB0F: malloc
# ==1234== by 0x10A3B2: myext_create (myext.c:45) ← 你的代码
# ==1234== by 0x5F3A21: PHP_FUNCTION
# PHP 自带 Valgrind 抑制文件
valgrind —suppressions=$(php–config —prefix)/share/php/valgrind.supp \\
php test.php
—–
AddressSanitizer(比 Valgrind 快):
# 编译时加上
CFLAGS="-fsanitize=address -g" \\
LDFLAGS="-fsanitize=address" \\
./configure —enable–debug
# 直接跑,有泄漏自动报
php your_script.php
# 输出
# =================================================================
# ==1234==ERROR: LeakSanitizer: detected memory leaks
#
# Direct leak of 1024 byte(s) in 1 object(s) allocated from:
# #0 malloc (in libasan.so)
# #1 myext_alloc myext.c:45 ← 定位到行
# #2 zif_myext_open myext.c:120
—–
扩展里常见泄漏场景:
// ❌ 场景1:分配了忘释放
PHP_FUNCTION(myext_get)
{
char *buf = emalloc(1024);
if (something_failed) {
RETURN_FALSE; // 直接返回,buf 泄漏
}
efree(buf);
RETURN_TRUE;
}
// ✅ 统一出口
PHP_FUNCTION(myext_get)
{
char *buf = emalloc(1024);
bool ok = false;
if (!something_failed) {
ok = true;
}
efree(buf); // 无论如何都释放
RETURN_BOOL(ok);
}
// ❌ 场景2:zend_string 引用计数没减
PHP_FUNCTION(myext_process)
{
zend_string *s = zend_string_init("hello", 5, 0);
// 用完忘了 release
RETURN_NULL();
// ✅
zend_string_release(s); // 减引用,为0才真正释放
RETURN_NULL();
}
// ❌ 场景3:HashTable 销毁了但内部数据没清
HashTable *ht = emalloc(sizeof(HashTable));
zend_hash_init(ht, 8, NULL, NULL, 0); // 析构函数传了NULL
zend_hash_add_ptr(ht, key, emalloc(64)); // 加进去的数据没人管
zend_hash_destroy(ht); // destroy 但析构函数是NULL,内部数据泄漏
efree(ht);
// ✅ 传正确的析构函数
zend_hash_init(ht, 8, NULL, efree, 0); // 或 ZVAL_PTR_DTOR
// ❌ 场景4:pemalloc 没有对应 pefree
PHP_MINIT_FUNCTION(myext) {
MY_G(cache) = pemalloc(sizeof(mycache), 1);
return SUCCESS;
}
// 忘了写 MSHUTDOWN
// ✅
PHP_MSHUTDOWN_FUNCTION(myext) {
pefree(MY_G(cache), 1);
MY_G(cache) = NULL;
return SUCCESS;
}
—–
运行时监控内存增长:
// 每次请求后检查内存有没有持续增长
$before = memory_get_usage();
// 跑业务逻辑
handle_request();
$after = memory_get_usage();
$leak = $after – $before;
if ($leak > 1024 * 1024) { // 增长超过 1MB 告警
error_log("Possible leak: {$leak} bytes after request");
}
// 长期监控,写到时序数据库
$peak = memory_get_peak_usage(true);
statsd_gauge('php.memory.peak', $peak);
// 画折线图,持续上涨 = 有泄漏
—–
扩展开发调试宏:
// zend_alloc.h 里的调试宏
#ifdef ZEND_DEBUG
// 分配时记录文件和行号
_emalloc(size, __FILE__, __LINE__, __FUNCTION__, 0)
// 检查某个指针是否有效
ZEND_ASSERT(ptr != NULL);
#endif
// 手动 dump 当前所有分配
zend_mm_heap *heap = zend_mm_get_heap();
// 在 –enable-debug 模式下有效
—–
一句话: 开发期用 —enable–debug 跑脚本看退出报告,深查用 Valgrind 或 ASan 定位到行,生产环境监控
memory_get_peak_usage 的趋势,持续涨就是漏了。




