內存泄漏指的是在程序運行過程中申請了內存,但是在使用完成后沒有及時釋放的現象,對于普通運行時間較短的程序來說可能問題不會那么明顯,但是對于長時間運行的程序,比如Web服務器,后臺進程等就比較明顯了,隨著系統運行占用的內存會持續上升,可能會因為占用內存過高而崩潰,或被系統殺掉([OOM](http://en.wikipedia.org/wiki/Out_of_memory))。
## PHP的內存泄漏[]()
PHP屬于高級語言,語言級別并沒有內存的概念,在使用過程中完全不需要主動申請或釋放內存,所以在PHP用戶代碼級別也就不存在內存泄漏的概念了。
但畢竟PHP是使用C編寫的解釋器,所以本質上還是一樣的,那么可以這么說:如果你的PHP程序內存泄漏了,肯定不是你的錯,而是PHP實現的錯:),當然也有可能是其他人的錯:很多公司都會有自己的PHP擴展,而擴展通常也使用C/C++來編寫,這樣擴展本身也可能會因為內存不正確釋放而導致內存泄漏。同時有些擴展是對第三方庫的一種包裹,比如PHP的sqlite數據庫操作接口主要是在libsqlite之上進行了封裝,所以如果libsqlite本身有內存泄漏的話,那也可能會帶來問題。
## 內存泄漏的debug及工具[]()
內存泄漏的程序通常很容易發現,因為癥狀都表現為內存占用的持續增長,在發現內存持續增長后我們需要判斷是什么導致了內存泄漏,這時往往需要借助一些工具來幫助追查,我們可以用到兩個工具:PHP內置內存泄漏探測及valgrind內存泄漏分析。
### PHP內置內存泄漏探測[]()
PHP本身有自己的內存管理,如果發現PHP有內存泄漏,可以嘗試重新編譯一個PHP,將編譯選項`--enable-debug`打開(同時所有的擴展也同樣需要編譯成支持debug模式的):`./configure --enable-debug`,這樣重新編譯后,如果PHP探測到有內存泄漏發生則會往[標準錯誤輸出](http://zh.wikipedia.org/wiki/Stderr#.E6.A8.99.E6.BA.96.E9.8C.AF.E8.AA.A4.E8.BC.B8.E5.87.BA_.28stderr.29)打印錯誤信息。這樣我們可以快速的發現問題。
在開啟debug模式下,PHP中會有一個函數`leak()`可以用于觸發內存泄漏,這個函數什么都不做,只是申請一塊內存但不釋放,其實現很簡單:
/* {{{ proto void leak(int num_bytes=3) Cause an intentional memory leak, for testing/debugging purposes */
ZEND_FUNCTION(leak)
{
long leakbytes=3;
?
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &leakbytes) == FAILURE) {
return;
}
?
emalloc(leakbytes); // 只申請,但是沒有釋放,這塊內存將會泄漏
}
/* }}} */
在下面的代碼中我們執行這個函數,然后看看PHP輸出的內容:
chapt06 git:(master) cat leak.php
<?php
leak();
?>
chapt06 git:(master) php leak.php
[Sat May 4 15:36:45 2013] Script: '/Users/reeze/www/tipi/book/chapt06/leak.php'
/Users/reeze/Opensource/php-test/php-src-5.4/Zend/zend_builtin_functions.c(1422) :
Freeing 0x106E07600 (3 bytes), script=/Users/reeze/www/tipi/book/chapt06/leak.php
=== Total 1 memory leaks detected ===
在上面我們看到PHP在最后輸出了在具體的那個代碼中出現了內存泄漏及出現的次數等。利用這個信息我們往往能快速的定位出到底是那里出現了問題。
根據PHP提供的泄漏信息你可能需要繼續追查到底是哪里的問題導致泄露了,如果最終發現是PHP的bug,那么很好,你可以編寫一個相應的修復方法,并提交到[http://bugs.php.net](http://bugs.php.net)這樣其他的PHP開發者可以跟進這個問題并最終修復到最新版的PHP中。詳細方式可以參考附錄D:怎么樣為PHP做貢獻。
> 本文使用的是cli命令行程序執行程序,當然如果你的程序是一個web應用,通常不太方便 直接使用php命令來執行,如果是這樣的話,那么你的PHP同樣會將錯誤信息輸出到標準錯誤輸出 如果你使用的是php-fpm的話,那么fpm會將錯誤重定向到日志文件`php-fpm.log`中。
由于命令行執行起來比較簡單,在追查問題過程中最好可以將你的代碼盡可能的精簡, 將精力集中正確的問題上。不過在用命令測試的時候最好確認你的PHP命令和你的web應用 使用的是同一個版本的PHP。
不過說到這里,前面提到的方法是有一些_代價和限制_的:
1. 首先這個方法有點麻煩,因為要重新編譯PHP代碼,同時還可能需要重新編譯你的所有擴展
1. 這個方法只能檢測到使用了Zend內存管理的情況,對于直接使用malloc/free來申請內存的應用或擴展是無法檢測到的。
雖然有以上的限制`--enable-debug`編譯選項在進行擴展或者PHP本身的開發時卻是很有用的,因為這樣能快速的發現問題,而對于生產環境來說,后面提到的valgrind分析法可能會更有效一點。
### valgrind輔助法[]()
[valgrind](http://valgrind.org/)是一個動態分析工具構建框架,可以用來分析程序的內存、線程等問題探測,程序性能分析等。具體的功能見官網,這是非常值得嘗試的工具。這里要使用的就是valgrind的內存錯誤分析工具。
我們大部分的時候使用的是Web模式,這時的調試相對麻煩一些。
由于我們需要發現PHP的內存泄漏,根據前面的章節我們知道PHP的內存分配是有一個內存池的,也就是說,并不是每次`emalloc`都會向操作系統申請內存,如果池有足夠的內存的話是會從池里進行分配的,而valgrind分析內存泄漏依賴的是內存的實際分配和實際釋放之間的關系,它會記下所有的`malloc`調用和`free`調用,如果出現不匹配的情況,那么就是發生了內存泄漏,所以在這種情況下我們需要將PHP的內存管理功能關閉才能不影響到我們的分析。
PHP提供了一個hook,我們可以在啟動PHP前指定`USE_ZEND_ALLOC`環境變量為0,即關閉內存管理功能。這樣所有的內存分配都會直接向操作系統申請,這樣valgrind就可以幫助我們定位問題。
valgrind程序可以這樣對一個程序進行內存分析
reeze@ubuntu:~$ export USE_ZEND_ALLOC=0 # 設置環境變量關閉內存管理
reeze@ubuntu:~$ cat leak.php
<?php
leak();
reeze@ubuntu:~$ valgrind --leak-check=full php leak.php
?
Memcheck, a memory error detector
Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
Command: php leak.php
?
HEAP SUMMARY:
in use at exit: 60 bytes in 3 blocks
total heap usage: 19,906 allocs, 19,903 frees, 3,782,702 bytes allocated
?
3 bytes in 1 blocks are definitely lost in loss record 1 of 3
at 0x4C2A66E: malloc (vg_replace_malloc.c:270)
by 0x80FC56: _emalloc (zend_alloc.c:2348)
by 0x858C7D: zif_leak (zend_builtin_functions.c:1346) # 檢測到我們的leak函數的泄漏
by 0x87C1CA: zend_do_fcall_common_helper_SPEC (zend_vm_execute.h:320)
by 0x8816BC: ZEND_DO_FCALL_SPEC_CONST_HANDLER (zend_vm_execute.h:1640)
by 0x87B17E: execute (zend_vm_execute.h:107)
by 0x840AF0: zend_execute_scripts (zend.c:1236)
by 0x7A1717: php_execute_script (main.c:2308)
by 0x9403E8: main (php_cli.c:1189)
?
LEAK SUMMARY:
definitely lost: 3 bytes in 1 blocks
indirectly lost: 0 bytes in 0 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 57 bytes in 2 blocks
suppressed: 0 bytes in 0 blocks
Reachable blocks (those to which a pointer was found) are not shown.
To see them, rerun with: --leak-check=full --show-reachable=yes
?
For counts of detected and suppressed errors, rerun with: -v
Use --track-origins=yes to see where uninitialised values come from
ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 2 from 2)
如上面所示,這里對某個php腳本執行并檢測內存泄漏,valgrind順利的發現了我們在leak函數中申請的內存并沒有正確的釋放,發現問題后修復起來就很簡單了。
這里是以命令行的方式來進行測試。對于web應用我們同樣可以用類似的方式來定位問題。已php-fpm為例,我們可以修改php-fpm啟動腳本,在啟動腳本中增加環境變量`USE_ZEND_ALLOC=0`以及將bin文件由原來的php-fpm文件修改為由valgrind啟動,并將valgrind的日志重定向到日志文件中,修改如下:
修改內容:
+ export USE_ZEND_ALLOC=0
+
- php_fpm_BIN="/usr/local/php/bin/php-fpm"
+ php_fpm_BIN="valgrind --log-file=/home/reeze/valgrind-log/%p.log /usr/local/php/bin/php-fpm"
修改好后使用:
$ php-fpm restart
重新訪問有內存泄漏嫌疑的頁面,這時指定的日志文件中就會有可能出現的內存泄露信息。前面示例中的`--log-file=/home/reeze/valgrind-log/%p.log`其中的`%p`是一個占位符,指的是進程號,所以比如運行起來的進程ID是1那么會將日志輸出到`1.log`文件,這主要是因為啟動的程序可能會`fork()`出多個子進程,這樣的好處是可以方便的知道具體是哪個進程的日志。
更多關于valgrind的使用還是建議閱讀相關的手冊。PHP官方也有對valgrind的使用的[說明](https://bugs.php.net/bugs-getting-valgrind-log.php)
## PHP的unclean shutdown[]()
前面提到的關于使用`USE_ZEND_ALLOC=0`來關閉PHP內存管理,這里就有一個疑問:將內存管理關掉出了性能上的差別還有其他差別么?
直覺上上理解,只是把內存管理關掉,直接向操作系統申請內存并沒有什么的,只是每次內存申請都需要想系統申請,只是效率變差了。
其實并不然,簡單講:PHP中的異常執行流依賴于內存管理來釋放內存。這里說的異常執行流指的是PHP實現級別的異常執行流,在用戶看來指的是出現PHP Fatal error或者內部異常時。
比如在出現PHP Fatal error時,PHP使用`longjmp`來進行跳轉,在C中我們通常使用配對的`malloc`和`free`來管理內存,但是使用`longjmp`后執行流發生了變化,可能會導致很多內存只進行了申請而無法正確釋放。
具體以文件`Zend/zend.c`中的`zend_call_destructors()`方法為例:
void zend_call_destructors(TSRMLS_D) /* {{{ */
{
zend_try {
shutdown_destructors(TSRMLS_C);
} zend_end_try();
}
/* }}} */
?
/* Zend/zend.h */
#define zend_try \
{ \
JMP_BUF *__orig_bailout = EG(bailout); \
JMP_BUF __bailout; \
\
EG(bailout) = &__bailout; \
if (SETJMP(__bailout)==0) {
#define zend_catch \
} else { \
EG(bailout) = __orig_bailout;
#define zend_end_try() \
} \
EG(bailout) = __orig_bailout; \
}
`zend_call_destructors`函數只在請求結束的最后執行所有對象的析構函數,由于我們的請求已經結束了,所以這里加了個try/catch防止執行的代碼拋出異常,如果拋出異常只是簡單的忽略它,因為我們已經不能做任何事情了。
內核實現中是使用`zend_bailout()`函數來實現"拋出異常"的。
ZEND_API void _zend_bailout(char *filename, uint lineno)
{
// ...
CG(unclean_shutdown) = 1; /* 標記 */
LONGJMP(*EG(bailout), FAILURE); // 跳轉至響應的catch位置
// ...
}
我們知道C語言并不支持異常,PHP中的try catch使用的是`jmp_buf`來模擬異常。
我們來看一下一個實際的例子:
reeze@ubuntu:~$ cat fatal.php
<?php
not_exists_func(); # 調用一個不存在的函數
reeze@ubuntu:~$ valgrind --leak-check=full php fatal.php
Memcheck, a memory error detector
Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
Command: php fatal.php
?
Fatal error: Call to undefined function not_exists_func() in /home/parallels/fatal.php on line 3
?
HEAP SUMMARY:
in use at exit: 686 bytes in 7 blocks
total heap usage: 19,910 allocs, 19,903 frees, 3,783,288 bytes allocated
?
628 (232 direct, 396 indirect) bytes in 1 blocks are definitely lost in loss record 7 of 7
at 0x4C2A66E: malloc (vg_replace_malloc.c:270)
by 0x80FC56: _emalloc (zend_alloc.c:2348)
by 0x7DFDA6: compile_file (zend_language_scanner.l:334)
by 0x60CBC5: phar_compile_file (phar.c:3397)
by 0x840997: zend_execute_scripts (zend.c:1228)
by 0x7A1717: php_execute_script (main.c:2308)
by 0x9403E8: main (php_cli.c:1189)
?
LEAK SUMMARY:
definitely lost: 232 bytes in 1 blocks # 內存泄漏
indirectly lost: 396 bytes in 4 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 58 bytes in 2 blocks
suppressed: 0 bytes in 0 blocks
Reachable blocks (those to which a pointer was found) are not shown.
To see them, rerun with: --leak-check=full --show-reachable=yes
?
For counts of detected and suppressed errors, rerun with: -v
Use --track-origins=yes to see where uninitialised values come from
ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 2 from 2)
這個腳本只是執行一個不存在的方法,最終拋出了致命錯誤而終止。這個看起來只是編碼錯誤,但是我們看到valgrind發現有內存泄漏,所以如果關閉了PHP的內存管理是有問題的,每次請求這個頁面都會導致內存不停的泄漏。
而如果PHP內存管理打開了之后,如果發生了這種異常情況下的長跳轉,PHP會將標志位:`CG(unclean_shutdown)`設置為true,在請求結束后會將所有的內存進行釋放:
// Zend/zend_alloc.c
ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, int full_shutdown, int silent TSRMLS_DC)
{
// ...
if (full_shutdown) { // full_shutdown == CG(unclean_shutdown)
// 釋放掉所有PHP堆棧中的內存
storage->handlers->dtor(storage);
if (!internal) {
free(heap);
}
} else {
if (heap->compact_size &&
heap->real_peak > heap->compact_size) {
storage->handlers->compact(storage);
}
heap->segments_list = NULL;
zend_mm_init(heap);
heap->real_size = 0;
heap->real_peak = 0;
heap->size = 0;
heap->peak = 0;
if (heap->reserve_size) {
heap->reserve = _zend_mm_alloc_int(heap, heap->reserve_size ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC);
}
heap->overflow = 0;
}
// ...
}
如果發了unclean shutdown并不是簡單的將內存回收到內存池,而是直接將所有的內存釋放以避免內存泄漏。這樣的好處是實現簡單,同時出現Fatal錯誤也是需要及時處理的問題。
## 參考[]()
1. Valgrind: [http://valgrind.org/](http://valgrind.org/)
1. C語言實現異常處理:[http://www.cnblogs.com/jiayu1016/archive/2012/10/20/2732712.html](http://www.cnblogs.com/jiayu1016/archive/2012/10/20/2732712.html)
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊