zval、引用計數、變量分離、寫時拷貝
咱們一步步來理解
一、php語言特性
PHP是腳本語言,所謂腳本語言,就是說PHP並非獨立運行的,要運行PHP代碼須要PHP解析器,用戶編寫的PHP代碼最終都會被PHP解析器解析執行
PHP的執行是經過Zend engine(ZE, Zend引擎),ZE是用C編寫的
用戶編寫的PHP代碼最終都會被翻譯成PHP的虛擬機ZE的虛擬指令(OPCODES)來執行
也就說最終會被翻譯成一條條的指令
既然這樣,有什麼結果和你預想的不同,查看php源碼是最直接最有效的php
二、php變量的存儲結構
在PHP中,全部的變量都是用一個結構zval結構來保存的,在Zend/zend.h中能夠看到zval的定義:算法
zval結構包括:
① value —— 值,是真正保存數據的關鍵部分,定義爲一個聯合體(union)
② type —— 用來儲存變量的類型
③ is_ref —— 下面介紹
④ refcount —— 下面介紹數組
聲明一個變量
$addr="北京";
PHP內部都是使用zval來表示變量的,那對於上面的腳本,ZE是如何把addr和內部的zval結構聯繫起來的呢?
變量都是有名字的(本例中變量名爲addr)
而zval中並無相應的字段來體現變量名。PHP內部確定有一個機制,來實現變量名到zval的映射
在PHP中,全部的變量都會存儲在一個數組中(確切的說是hash table)
當你建立一個變量的時候,PHP會爲這個變量分配一個zval,填入相應的信息,而後將這個變量的名字和指向這個zval的指針填入一個數組中。當你獲取這個變量的時候,PHP會經過查找這個數組,取得對應的zval服務器
注意:數組和對象這類複合類型在生成zval時,會爲每一個單元生成一個zval函數
三、咱們常常說每一個變量都有一個內存地址,那這個zval和變量的內存地址,這倆有什麼關係嗎?
定義一個變量會開闢一塊內存,這塊內存比如一個盒子,盒子裏放了zval,zval裏保存了變量的相關信息,須要開闢多大的內存,是由zval所佔空間大小決定的
zval是內存對象,垃圾回收的時候會把zval和內存地址(盒子)分別釋放掉測試
四、引用計數、變量分離、寫時拷貝
zval中的refcount和is_ref尚未介紹,咱們知道PHP是一個長時間運行的服務器端腳本。那麼對於它來講,效率和資源佔用率是一個很重要的衡量標準,也就是說,PHP必須儘可能減小內存佔用率。考慮下面這段代碼:spa
第一行代碼建立了一個字符串變量,申請了一個大小爲9字節的內存,保存了字符串「laruence」和一個NULL(\0)的結尾
第二行定義了一個新的字符串變量,並將變量var的值「複製」給這個新的變量
第三行unset了變量var翻譯
這樣的代碼是很常見的,若是PHP對於每個變量賦值都從新分配內存,copy數據的話,那麼上面的這段代碼就要申請18個字節的內存空間,爲了申請新的內存,還須要cpu執行某些計算,這固然會加劇cpu的負載
而咱們也很容易看出來,上面的代碼其實根本沒有必要申請兩份空間,當第三句執行後,$var被釋放了,咱們剛纔的設想(申請18個字節內存空間)忽然變的很滑稽,此次複製顯得好多餘。若是早知道$var不用了,直接讓$var_dup用$var的內存不就好了,還複製幹嗎?若是你以爲9個字節沒什麼,那設想下若是$var是個10M的文件內容,或者20M,是否是咱們的計算機資源消耗的有點冤枉呢?
呵呵,PHP的開發者也看出來了:debug
剛纔說了,PHP中的變量是用一個存儲在symbol_table中的符號名,對應一個zval來實現的,好比對於上面的第一行代碼,會在symbol_table中存儲一個值「var」,對應的有一個指針指向一個zval結構,變量值「laruence」保存在這個zval中,因此不難想象,對於上面的代碼來講,咱們徹底可讓「var」和「var_dup」對應的指針都指向同一個zval就能夠了(額,鳥哥一會說hash table,一會說symbol_table,暫且理解爲symbol_table是hash table的子集)指針
PHP也是這樣作的,這個時候就須要介紹一下zval結構中的refcount字段了
refcount,引用計數,記錄了當前的zval被引用的次數(這裏的引用並非真正的 & ,而是有幾個變量指向它)
好比對於代碼:
第一行,建立了一個整形變量,變量值是1。 此時保存整形1的這個zval的refcount爲1
第二行,建立了一個新的整形變量(經過賦值的方式),變量也指向剛纔建立的zval,並將這個zval的refcount加1,此時這個zval的refcount爲2
因此,這個時候(經過值傳遞的方式賦值給別的變量),並無產生新的zval,兩個變量指向同一zval,經過一個計數器來共用zval及內存地址,以達到節省內存空間的目的
當一個變量被第一次建立的時候,它對應的zval結構的refcount的值會被初始化爲1,由於只有這一個變量在用它。可是當你把這個變量賦值給別的變量時,refcount屬性便會加1變成2,由於如今有兩個變量在用這個zval結構了
PHP提供了一個函數能夠幫助咱們瞭解這個過程debug_zval_dump
輸出:
long(1) refcount(2)
long(1) refcount(3)
若是你奇怪 ,var的refcount應該是1啊?
咱們知道,對於簡單變量,PHP是以傳值的形式傳參數的。也就是說,當執行debug_zval_dump($var)的時候,$var會以傳值的方式傳遞給debug_zval_dump,也就是會致使var的refcount加1,因此只要能看到,當變量賦值給一個變量之後,能致使zval的refcount加1這個結果便可
如今咱們回頭看上面的代碼, 當執行了最後一行unset($var)之後,會發生什麼呢?
unset($var)的時候,它刪除符號表裏的$var的信息,準備清理它對應的zval及內存空間,這時它發現$var對應的zval結構的refcount值是2,也就是說,還有另一個變量在一塊兒用着這個zval,因此unset只需把這個zval的refcount減去1就好了
上代碼:
輸出:
string(8) "laruence" refcount(2)
可是,對於下面的代碼呢?
很明顯在這段代碼執行之後,$var_dup的值應該仍是「laruence」,那麼這又是怎麼實現的呢?
這就是PHP的copy on write機制(簡稱COW):
PHP在修改一個變量之前,會首先查看這個變量的refcount,若是refcount大於1,PHP就會執行一個分離的過程(在Zend引擎中,分離是破壞一個引用對的過程)
對於上面的代碼,當執行到第三行的時候,PHP發現$var想要改變,而且它指向的zval的refcount大於1,那麼PHP就會複製一個新的zval出來,改變其值,將改變的變量指向新的zval(哪一個變量指向新複製的zval其實已經無所謂了),並將原zval的refcount減1,並修改symbol_table裏該變量的指針,使得$var和$var_dup分離(Separation)。這個機制就是所謂的copy on write(寫時複製,這裏的寫包括普通變量的修改及數組對象裏的增長、刪除單元操做)
若是瞭解了is_ref以後,上面說的並不嚴謹
上代碼測試:
輸出:
long(1) refcount(2)
string(8) "laruence" refcount(2)
如今咱們知道,當使用變量複製的時候 ,PHP內部並非真正的複製,而是採用指向相同的zval結構來節約開銷。那麼,對於PHP中的引用,又是如何實現呢?
這段代碼結束之後,$var也會被間接的修改成1,這個過程稱做(change on write:寫時改變)
那麼ZE是怎麼知道,此次的複製不須要Separation呢?
這個時候就要用到zval中的is_ref字段了:
對於上面的代碼,當第二行執行之後,$var所表明的zval的refcount變爲2,而且設置is_ref爲1
到第三行的時候,PHP先檢查var_ref對應的zval的is_ref字段(is_ref 表示該zval是否被&引用,僅表示真或假,就像開關的開與關同樣,zval的初始化狀況下爲0,即非引用),若是爲1,則不分離,直接更改(不然須要執行剛剛提到的zval分離),更改共享的zval實際上也間接更改了$var的值,由於引擎想全部的引用變量都看到這一改變
php源碼作了這樣一個判斷,大致邏輯示意以下:
若是這個zval中的if_ref爲1(即被引用),或者該zval引用計數小於2
任何一種方式:都不會進行分離
儘管已經存在寫時複製和寫時改變,但仍然還存在一些不能經過is_ref和refcount來解決的問題
對於以下的代碼,又會怎樣呢?
這裏$var、$var_dup、$var_ref三個變量將共用一個zval結構(其實這是不可能的,一個zval不可能既被&,又被指向),有兩個屬於change-on-write組合($var和$var_ref),有兩個屬於copy-on-write組合($var和$var_dup),那is_ref和refcount該怎樣工做,才能正確的處理好這段複雜的關係呢?
答案是不可能!在這種狀況下,變量的值必須分離成兩份徹底獨立的存在
當執行第二行代碼的時候,和前面講過的同樣,$var_dup 和 $var 指向相同的zval, refcount爲2
當執行第三行的時候,PHP發現要操做的zval的refcount大於1,則PHP會執行Separation(也就是說php將一個zval的is_ref從0設爲1 以前,固然此時refcount尚未增長,會看該zval的refcount,若是refcount>1,則會分離), 將$var_dup分離出去,並將$var和$var_ref作change on write關聯。也就是,refcount=2, is_ref=1;
因此內存會給變量var_dup 分配出一個新的zval,類型與值同 $var和$var_ref指向的zval同樣,是新分配出來的,儘管他們擁有一樣的值,可是必須經過兩個zval來實現。試想一下,若是三者指向同一個zval的話,改邊 $var_dup 的值,那麼 $var和$var_ref 也會受到影響,這樣就亂套了
圖解:
下面的這段代碼在內核中一樣會產生歧義,因此須要強制複製!
也就是說一個zval不會既被引用,又被指向,必須分離
基於這樣的分析,咱們就可讓debug_zval_dump出refcount爲1的結果來:
輸出:
string(8) "laruence" refcount(1)
爲何結果是refcount(1)呢
debug_zval_dump()中參數是引用的話,refcount永遠爲1
小結:
這兩段代碼在執行的時候是這樣的邏輯:
PHP先看變量指向的zval是否被引用,若是是引用,則再也不產生新的zval
甭管哪一個變量引用了它,好比有個變量$a被引用了,$b=&$a,就算本身引用本身$a=&$a,$a所指向的zval都不會被複制,改變其中一個變量的值,另外一個值也被改變(寫時改變)
若是is_ref爲0且refcount大於1,改變其中一個變量時,複製新的zval(寫時複製)
五、垃圾回收概述
refcount和is_ref這兩個傢伙與垃圾回收有關(garbage collection簡稱gc)
PHP的垃圾回收全靠這倆字段了。其中refcount表示當前有幾個變量引用此zval,而is_ref表示當前zval是否被按引用引用
PHP5.2中的垃圾回收算法 —— Reference Counting
PHP5.2中使用的內存回收算法是大名鼎鼎的Reference Counting,這個算法中文翻譯叫作「引用計數」,其思想很是直觀和簡潔:爲每一個內存對象分配一個計數器,當一個內存對象創建時計數器初始化爲1(此時老是有一個變量引用此對象),之後每有一個新變量引用此內存對象,則計數器加1,而每當減小一個引用此內存對象的變量則計數器減1,任何關聯到某個zval的變量離開它的做用域(好比:函數執行結束),或者把變量unset掉,refcount也會減1
當垃圾回收機制運做的時候,將全部計數器爲0的內存對象銷燬並回收其佔用的內存。而PHP中內存對象就是zval,計數器就是refcount
Reference Counting簡單直觀,實現方便,但卻存在一個致命的缺陷,就是容易形成內存泄露(具體緣由百度)
因爲Reference Counting的這個缺陷,PHP5.3改進了垃圾回收算法
PHP5.3的垃圾回收算法仍然以引用計數爲基礎,可是再也不是使用簡單計數做爲回收準則,而是使用了一種同步回收算法,這個算法由IBM的工程師在論文Concurrent Cycle Collection in Reference Counted Systems中提出
這裏只須要了解垃圾回收是以引用計數爲基礎的就能夠