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++裡面也同樣存在,就是在設計類的時候,絕不要把錯誤處理或警示函數寫在構造或者解構函式內,同樣的原因,由於對象已經處在了銷毀或建立的階段,所以任何錯誤函數處理都可能打斷這一過程,從而可能導致記憶體流失。) 下面的代碼中就給出了這樣的一個例子:
<span style="font-family:SimSun;">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);}</span>
在這個例子中,提供了一個php在調用函數時候的功能。當php調用函數時,需要到函數表也就是function_table中去尋找相應的函數,而在尋找之前要先轉換到小寫字母,這樣在尋找的時候可以提高尋找的效率。 而通過zend_hash_find函數如果找到了要調用的函數,就使用zend_execute進行調用。而如果沒找到的haunted就要跳出報錯,顯示沒找到。但是問題來了,注意之前為了尋找函數建立了一個小寫版本的函數名字串。這個字串一直到用到zend_hash_find函數,一旦沒找到進入了報錯之後,那麼這個字串所對應的記憶體空間必然就找不回來了,這就造成了記憶體的泄露。
因此,php提供了
Zend記憶體管理,Zend memory management也稱為ZendMM。
php中的記憶體管理與作業系統的機制類似,但是對象是針對每一個請求所涉及的記憶體的。 除此之外ZendMM還會控制ini檔案裡面規定的memory_limit,也就是說一旦每個請求所要求的記憶體超過了這個memory limit,那麼也會申請失敗。 在圖中的最下面看到了它與作業系統相聯絡的一層。針對作業系統中的標準的記憶體申請和釋放的方法,php中都有對應的函數。這些函數並不是一個簡單的替換,它們中包含有特定的資訊,在這些資訊的協助下就能夠把每個請求所申請的記憶體塊進行標識。這樣就能夠實現對每個請求的記憶體地區進行分別的管理。 同時在圖中看到了一共兩種記憶體請求的方式:persistent和per-request,對於persistent來說差不多跟系統的請求就一樣了,也就是說是獨立在每一個請求之外的,不會在請求結束之後被回收。但是有時候是否persistent可能要runtime才能知道,所以在這種情況下,需要一個flag來指示這一點。對於是否是persistent,進行記憶體請求的方式是不一樣的。下面給出對應關係: pemalloc(buffer_len,1) == malloc(buffer_len) permalloc(buffer_len,0) == emalloc(buffer_len)這種聯絡是用宏定義的方式決定的 #define pemalloc(size,persistent) \ ((persistent)?malloc(size):emalloc(size)) flag=1表示是persistent的,為0表示不是,就跟一般的附屬於請求的emalloc一樣了。
下圖中可以看到系統的記憶體申請函數與php中的記憶體申請函數的對比轉換圖:
如果你對malloc、calloc和realloc這些函數還不太熟悉,請移步: http://www.cppblog.com/sandywin/archive/2011/09/14/155746.html
除此之外,還有兩個
安全模式的記憶體函數: void *safe_emalloc(size_t size, size_t count, size_t addtl);
void *safe_pemalloc(size_t size, size_t count, size_t addtl, char persistent); 他們申請的空間是 size*count + addtl,存在的原因是為了避免int型的溢出。
接下來說一個更有趣的,php中的
引用計數:
很多語言中都有引用,很多時候也都會使用引用。通過引用可以節省空間的,因為有時候並沒有必要為每個變數都製造一個副本。 所謂引用計數,就是指同一塊記憶體空間被多少個變數引用了,從而避免可能的記憶體錯誤操作。 先看下面的一段代碼:
* {* 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);* <strong>ZVAL_ADDREF(helloval);</strong>//加上這個之後,就不會有重新釋放同一塊記憶體空間這樣的錯誤了* zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),* &helloval, sizeof(zval*), NULL);* }
進行了ZVAL_ADDREF之後,下一次unset變數的時候,會先查看ref_count引用計數,如果=1就釋放,如果>1就只是-1,並不進行記憶體釋放。
Copy on Write 再來看下面的這一段php代碼:
<?php $a = 1; $b = $a; $b += 5;?>
很顯然在第二行的時候b聲明了一個a的引用,那麼在執行完了第三行的代碼之後,b增加了,a增不增加呢。很多時候可能並不想增加。所以這個時候當Zend檢測到refCount>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。這樣的話就是兩塊獨立的記憶體塊了。
Change on Write 再看一個程式碼片段:
<?php $a = 1;//執行完這一句之後,a變數的ref_count是1,is_ref是0 $b = &$a;//這一句之後,變數(zval*)的ref_count是2,然後由於顯示的&,is_ref為1 $b += 5;// 這個時候在執行這一句的時候就不會有任何的分離?>
如果你覺得想要a跟著b一起改變,那沒有問題,只要顯式的用&符號進行引用聲明就可以了。這樣的話is_ref標誌位就會被置1. 這時候也就沒必要進行記憶體塊的分離了。所以在上面的代碼中要把第二個if語句的判斷更改一下:
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;}
再看最後一種情況,這種情況最糾結:
<?php $a = 1; $b = $a; $c = &$a;?>
既不是copy on write也不是change on wirte,那沒辦法了,只好分離一下。這裡只好b獨立出來了:
對php記憶體管理的一些機制就說到這裡,感覺php確實是一門相當神奇的語言。哈哈。
原文連結http://meijing0114.com/2014/12/13/2/