php對內存的管理機制相當的詳盡,它在這一點上更類似與java的垃圾回收機制。而對於c語言或者c++大部分時候都只能由程序員自己把申請的空間釋放掉。在php中,由於要應對成千上萬的連接,同時這些連接往往還需要保持很長的時間。這並不同於c中程序結束了相應的內存塊就會被回收。
所以僅僅依靠程序員在寫程序的時候注意內存回收是不夠的,php肯定要有一些自己內部的、與連接相關的內存管理機制來保證不發生任何的內存洩露。
在本文中,首先對php的內存機制進行一個介紹:
那些在c語言中的空間函數,比如malloc() free() strdup() realloc() calloc(),php中會有不同的形式。
返還申請的內存:對於程序員來說,每一塊申請的內存都應該返還,如果不還就會導致內存洩漏。在那些不要求一直運行的程序中,稍許的內存洩漏在整個進程被殺掉之後就結束了。但是類似於apache這種一直運行的web server,小的內存洩漏最終會導致程序的崩潰。
錯誤處理的例子:
在進行錯誤處理的時候,采用的機制一般是是Zend Engine會設定一個跳出地址,一旦發生exit或die或任何嚴重錯誤E_ERROR的時候,就會利用一個longjmp()跳到這個地址上面去。但是這種做法幾乎都會導致內存洩漏。因為free的操作都會被跳掉。(這個問題在c++裡面也同樣存在,就是在設計類的時候,絕不要把錯誤處理或告警函數寫在構造或者析構函數內,同樣的原因,由於對象已經處在了銷毀或創建的階段,所以任何錯誤函數處理都可能打斷這一過程,從而可能導致內存洩漏。) 下面的代碼中就給出了這樣的一個例子:void call_function(const char *fname, int fname_len TSRMLS_DC) { zend_function *fe; char *lcase_fname; /* PHP function names are case-insensitive to simplify locating them in the function tables all function names are implicitly * translated to lowercase */ lcase_fname = estrndup(fname, fname_len);//創造一個函數名的副本 zend_str_tolower(lcase_fname, fname_len);//都轉換成小寫,這樣的尋找的時候很方便,這應該也是php函數表中進行函數標識的方式。 if (zend_hash_find(EG(function_table), lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {?SUCCESS。這個是要在函數表裡面尋找待調用的函數。 zend_execute(fe->op_array TSRMLS_CC); } else { php_error_docref(NULL TSRMLS_CC, E_ERROR, "Call to undefined function: %s()", fname); //等同於Trigger_error() } efree(lcase_fname); }在這個例子中,提供了一個php在調用函數時候的功能。當php調用函數時,需要到函數表也就是function_table中去尋找相應的函數,而在尋找之前要先轉換到小寫字母,這樣在尋找的時候可以提高查找的效率。 而通過zend_hash_find函數如果找到了要調用的函數,就使用zend_execute進行調用。而如果沒找到的haunted就要跳出報錯,顯示沒找到。但是問題來了,注意之前為了尋找函數創建了一個小寫版本的函數名字符串。這個字符串一直到用到zend_hash_find函數,一旦沒找到進入了報錯之後,那麼這個字符串所對應的內存空間必然就找不回來了,這就造成了內存的洩露。
* { * zval *helloval; * MAKE_STD_ZVAL(helloval); * ZVAL_STRING(helloval, "Hello World", 1); * zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), * &helloval, sizeof(zval*), NULL); * zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), * &helloval, sizeof(zval*), NULL); * }這段代碼首先聲明了一個zval變量,再用MAKE_STD_ZVAL進行了初始化,接下來用ZVAL_STRING附了初值。然後對這個變量,給出了兩個變量名。第一個是a,第二個是b,毫無疑問,第二個肯定是一個引用。但是這段代碼這麼寫肯定有問題,問題就在於你在用zend_hash_add之後並沒有更新相應的引用計數。zend並不知道你多加了這麼一個引用,這就導致釋放內存的時候可能導致兩次釋放。所以經過修改之後的正確代碼如下:
* { * zval *helloval; * MAKE_STD_ZVAL(helloval); * ZVAL_STRING(helloval, "Hello World", 1); * zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), * &helloval, sizeof(zval*), NULL); * ZVAL_ADDREF(helloval);//加上這個之後,就不會有重新釋放同一塊內存空間這樣的錯誤了 * zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), * &helloval, sizeof(zval*), NULL); * }進行了ZVAL_ADDREF之後,下一次unset變量的時候,會先查看ref_count引用計數,如果=1就釋放,如果>1就只是-1,並不進行內存釋放。
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC) { zval **varval, *varcopy; if (zend_hash_find(EG(active_symbol_table), varname, varname_len + 1, (void**)&varval) == FAILURE) { /*符號表裡沒找到 */ return NULL; } if ((*varval)->refcount < 2) { /* varname 是唯一的引用,什麼也不用做 */ return *varval; } /* 否則的話,不是唯一的引用,給zval*做一個副本 */ MAKE_STD_ZVAL(varcopy); varcopy = *varval; /* Duplicate any allocated structures within the zval* */ zval_copy_ctor(varcopy); //這一塊是怎麼拷貝的?mark 應該已經跟varval對應的varname連起來了 /* 把varname的版本刪掉,這會減少varval的引用次數 */ zend_hash_del(EG(active_symbol_table), varname, varname_len + 1); /* 初始化新創造的值的引用次數,然後附給varname變量 */ varcopy->refcount = 1; varcopy->is_ref = 0; zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &varcopy, sizeof(zval*), NULL); /* Return the new zval* */ return varcopy; }首先看到了兩個判斷語句,第一個判斷語句先在符號表裡面看看有沒有找到相應的變量,如果沒找到也就沒必要分離了。第二個判斷語句是看輸入的變量的引用次數是不是小於2,如果是的話那就說明輸入變量*varval是唯一的,也沒必要分離。 否則的話肯定有引用,這個時候就要制作一個副本varcopy。這個副本會承襲varname對應的值,但是不同之處在於幫它重新申請了內存空間,重新初始化了refcount和is_ref參數。 以a、b為例,在$b+=5,執行之後,b作為varname去尋找是否有引用,發現還有一個引用a,這個時候就把b的值拷出來,然後重新申請一片空間,在重新注冊為b。這樣的話就是兩塊獨立的內存塊了。
if ((*varval)->is_ref || (*varval)->refcount < 2) { /* varname is the only actual reference, * or it's a full reference to other variables * either way: no separating to be done */ return *varval; }