php zval_copy_static_var(),zval _ 引用计数 _ 变量分离 _ 写时拷贝
zval、引用計數、變量分離、寫時拷貝
我們一步步來理解
1、php語言特性
PHP是腳本語言,所謂腳本語言,就是說PHP并不是獨立運行的,要運行PHP代碼需要PHP解析器,用戶編寫的PHP代碼最終都會被PHP解析器解析執行
PHP的執行是通過Zend engine(ZE, Zend引擎),ZE是用C編寫的
用戶編寫的PHP代碼最終都會被翻譯成PHP的虛擬機ZE的虛擬指令(OPCODES)來執行
也就說最終會被翻譯成一條條的指令
既然這樣,有什么結果和你預想的不一樣,查看php源碼是最直接最有效的
2、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
3、我們經常說每個變量都有一個內存地址,那這個zval和變量的內存地址,這倆有什么關系嗎?
定義一個變量會開辟一塊內存,這塊內存好比一個盒子,盒子里放了zval,zval里保存了變量的相關信息,需要開辟多大的內存,是由zval所占空間大小決定的
zval是內存對象,垃圾回收的時候會把zval和內存地址(盒子)分別釋放掉
4、引用計數、變量分離、寫時拷貝
zval中的refcount和is_ref還沒有介紹,我們知道PHP是一個長時間運行的服務器端腳本。那么對于它來說,效率和資源占用率是一個很重要的衡量標準,也就是說,PHP必須盡量減少內存占用率。考慮下面這段代碼:
第一行代碼創建了一個字符串變量,申請了一個大小為9字節的內存,保存了字符串“laruence”和一個NULL(\0)的結尾
第二行定義了一個新的字符串變量,并將變量var的值“復制”給這個新的變量
第三行unset了變量var
這樣的代碼是很常見的,如果PHP對于每一個變量賦值都重新分配內存,copy數據的話,那么上面的這段代碼就要申請18個字節的內存空間,為了申請新的內存,還需要cpu執行某些計算,這當然會加重cpu的負載
而我們也很容易看出來,上面的代碼其實根本沒有必要申請兩份空間,當第三句執行后,$var被釋放了,我們剛才的設想(申請18個字節內存空間)突然變的很滑稽,這次復制顯得好多余。如果早知道$var不用了,直接讓$var_dup用$var的內存不就行了,還復制干嘛?如果你覺得9個字節沒什么,那設想下如果$var是個10M的文件內容,或者20M,是不是我們的計算機資源消耗的有點冤枉呢?
呵呵,PHP的開發者也看出來了:
剛才說了,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(寫時復制)
5、垃圾回收概述
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中提出
這里只需要了解垃圾回收是以引用計數為基礎的就可以
總結
以上是生活随笔為你收集整理的php zval_copy_static_var(),zval _ 引用计数 _ 变量分离 _ 写时拷贝的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php水平线代码,在HTML中水平线标注
- 下一篇: php实现工厂模式,PHP基于工厂模式实