解析PHP7核心之變數的內部實現

2020-07-16 10:06:14

PHP變數實現的基礎結構是zval,各種型別的實現均基於此結構實現,是PHP中最基礎的一個結構,每個PHP變數都對應一個zval,下面就看下這個結構以及PHP變數的記憶體管理機制。

zval結構

zval結構比較簡單,內嵌一個union型別的zend_value儲存具體變數型別的值或指標,zval中還有兩個union:u1、u2:
  • u1:它的意義比較直觀,變數的型別就通過u1.type區分,另外一個值type_flags為型別掩碼,在變數的記憶體管理、gc機制中會用到,第三部分會詳細分析,至於後面兩個const_flagsreserved暫且不管

  • u2:這個值純粹是個輔助值,假如zval只有:valueu1兩個值,整個zval的大小也會對齊到16byte,既然不管有沒有u2大小都是16byte,把多餘的4byte拿出來用於一些特殊用途還是很划算的,比如next在雜湊表解決雜湊衝突時會用到,還有fe_pos在foreach會用到......

zend_value可以看出,除longdouble型別直接儲存值外,其它型別都為指標,指向各自的結構。


型別

zval.u1.type型別:

標量型別

最簡單的型別是true、false、long、double、null,其中true、false、null沒有value,直接根據type區分,而long、double的值則直接存在value中:zend_long、double,也就是標量型別不需要額外的value指標。

字串

PHP中字串通過zend_string表示:

  • gc:變數參照資訊,比如當前value的參照數,所有用到參照計數的變數型別都會有這個結構,3.1節會詳細分析

  • h:雜湊值,陣列中計算索引時會用到

  • len:字串長度,通過這個值保證二進位制安全

  • val:字串內容,變長struct,分配時按len長度申請記憶體

事實上字串又可具體分為幾類:IS_STR_PERSISTENT(通過malloc分配的)、IS_STR_INTERNED(php程式碼裡寫的一些字面量,比如函數名、變數值)、IS_STR_PERMANENT(永久值,生命週期大於request)、IS_STR_CONSTANT(常數)、IS_STR_CONSTANT_UNQUALIFIED,這個資訊通過flag儲存:zval.value->gc.u.flags,後面用到的時候再具體分析。

陣列

array是PHP中非常強大的一個資料結構,它的底層實現就是普通的有序HashTable,這裡簡單看下它的結構,下一節會單獨分析陣列的實現。


物件/資源


物件比較常見,資源指的是tcp連線、檔案控制代碼等等型別,這種型別比較靈活,可以隨意定義struct,通過ptr指向,後面會單獨分析這種型別,這裡不再多說。


參照

參照是PHP中比較特殊的一種型別,它實際是指向另外一個PHP變數,對它的修改會直接改動實際指向的zval,可以簡單的理解為C中的指標,在PHP中通過&操作符產生一個參照變數,也就是說不管以前的型別是什麼,&首先會將新生成一個zval,型別為IS_REFERENCE,然後將val的value指向原來zval的value。

結構非常簡單,除了公共部分zend_refcounted_h外只有一個val,舉個範例看下具體的結構關係:

最終的結果如圖:


注意:參照只能通過&產生,無法通過賦值傳遞,比如:



$b = &$a這時候$a$b的型別是參照,但是$c = $b並不會直接將$b賦值給$c,而是把$b實際指向的zval賦值給$c,如果想要$c也是一個參照則需要這麼操作:

這個也表示PHP中的參照只可能有一層不會出現一個參照指向另外一個參照的情況,也就是沒有C語言中指標的指標的概念。

記憶體管理

接下來分析下變數的分配、銷毀。

在分析變數記憶體管理之前我們先自己想一下可能的實現方案,最簡單的處理方式:定義變數時alloc一個zval及對應的value結構(ref/arr/str/res...),賦值、函數傳參時硬拷貝一個副本,這樣各變數最終的值完全都是獨立的,不會出現多個變數同時共用一個value的情況,在執行完以後直接將各變數及value結構free掉。

這種方式是可行的,而且記憶體管理也很簡單,但是,硬拷貝帶來的一個問題是效率低,比如我們定義了一個變數然後賦值給另外一個變數,可能後面都只是唯讀操作,假如硬拷貝的話就會有多餘的一份資料,這個問題的解決方案是:參照計數+寫時複製。PHP變數的管理正是基於這兩點實現的。

參照計數

參照計數是指在value中增加一個欄位refcount記錄指向當前value的數量,變數複製、函數傳參時並不直接硬拷貝一份value資料,而是將refcount++,變數銷毀時將refcount--,等到refcount減為0時表示已經沒有變數參照這個value,將它銷毀即可。

參照計數的資訊位於給具體value結構的gc中:

從上面的zend_value結構可以看出並不是所有的資料型別都會用到參照計數,longdouble直接都是硬拷貝,只有value是指標的那幾種型別才可能會用到參照計數。

下面再看一個例子:

$a = "hi~";$b = $a;

猜測一下變數$a/$b的參照情況。

這個不跟上面的例子一樣嗎?字串"hi~"$a/$b兩個參照,所以zend_string1(refcount=2)。但是這是錯的,gdb偵錯發現上面例子zend_string的參照計數為0。這是為什麼呢?

$a,$b -> zend_string_1(refcount=0,val="hi~")

事實上並不是所有的PHP變數都會用到參照計數,標量:true/false/double/long/null是硬拷貝自然不需要這種機制,但是除了這幾個還有兩個特殊的型別也不會用到:interned string(內部字串,就是上面提到的字串flag:IS_STR_INTERNED)、immutable array,它們的type是IS_STRINGIS_ARRAY,與普通string、array型別相同,那怎麼區分一個value是否支援參照計數呢?還記得zval.u1中那個型別掩碼type_flag嗎?正是通過這個欄位標識的,這個欄位除了標識value是否支援參照計數外還有其它幾個標識位,按位元分割,注意:type_flagzval.value->gc.u.flag不是一個值。

支援參照計數的value型別其zval.u1.type_flag包含(注意是&,不是等於)IS_TYPE_REFCOUNTED

#define IS_TYPE_REFCOUNTED          (1<<2)

下面具體列下哪些型別會有這個標識:

|     type       | refcounted |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |      Y     |
|resource        |      Y     |
|reference       |      Y     |

simple types很顯然用不到,不再解釋,string、array、object、resource、reference有參照計數機制也很容易理解,下面具體解釋下另外兩個特殊的型別:

  • interned string:內部字串,這是種什麼型別?我們在PHP中寫的所有字元都可以認為是這種型別,比如function name、class name、variable name、靜態字串等等,我們這樣定義:$a = "hi~;"後面的字串內容是唯一不變的,這些字串等同於C語言中定義在靜態變數區的字串:char *a = "hi~";,這些字串的生命週期為request期間,request完成後會統一銷毀釋放,自然也就無需在執行期間通過參照計數管理記憶體。

  • immutable array:只有在用opcache的時候才會用到這種型別,不清楚具體實現,暫時忽略。


寫時複製

上一小節介紹了參照計數,多個變數可能指向同一個value,然後通過refcount統計參照數,這時候如果其中一個變數試圖更改value的內容則會重新拷貝一份value修改,同時斷開舊的指向,寫時複製的機制在計算機系統中有非常廣的應用,它只有在必要的時候(寫)才會發生硬拷貝,可以很好的提高效率,下面從範例看下:

$a = array(1,2);$b = &$a;$c = $a;//發生分離$b[] = 3;


最終的結果:

不是所有型別都可以copy的,比如物件、資源,實時上只有string、array兩種支援,與參照計數相同,也是通過zval.u1.type_flag標識value是否可複製的:

#define IS_TYPE_COLLECTABLE         (1<<3)
|     type       |  copyable  |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |            |
|resource        |            |
|reference       |            |

copyable的意思是當value發生duplication時是否需要copy,這個具體有兩種情形下會發生:

  • a.從literal變數區複製到區域性變數區,比如:$a = [];實際會有兩個陣列,而$a = "hi~";//interned string則只有一個string

  • b.區域性變數區分離時(寫時複製):如改變變數內容時參照計數大於1則需要分離,$a = [];$b = $a; $b[] = 1;這裡會分離,型別是array所以可以複製,如果是物件:$a = new user;$b = $a;$a->name = "dd";這種情況是不會複製object的,$a、$b指向的物件還是同一個

具體literal、區域性變數區變數的初始化、賦值後面編譯、執行兩篇文章會具體分析,這裡知道變數有個copyable的屬性就行了。

變數回收

PHP變數的回收主要有兩種:主動銷毀、自動銷毀。主動銷毀指的就是unset,而自動銷毀就是PHP的自動管理機制,在return時減掉區域性變數的refcount,即使沒有顯式的return,PHP也會自動給加上這個操作。

垃圾回收

PHP變數的回收是根據refcount實現的,當unset、return時會將變數的參照計數減掉,如果refcount減到0則直接釋放value,這是變數的簡單gc過程,但是實際過程中出現gc無法回收導致記憶體漏失的bug,先看下一個例子:

$a = [1];$a[] = &$a;unset($a);

unset($a)之前參照關係:

unset($a)之後:

可以看到,unset($a)之後由於陣列中有子元素指向$a,所以refcount > 0,無法通過簡單的gc機制回收,這種變數就是垃圾,垃圾回收器要處理的就是這種情況,目前垃圾只會出現在array、object兩種型別中,所以只會針對這兩種情況作特殊處理:當銷毀一個變數時,如果發現減掉refcount後仍然大於0,且型別是IS_ARRAY、IS_OBJECT則將此value放入gc可能垃圾雙向連結串列中,等這個連結串列達到一定數量後啟動檢查程式將所有變數檢查一遍,如果確定是垃圾則銷毀釋放。

標識變數是否需要回收也是通過u1.type_flag區分的:

#define IS_TYPE_COLLECTABLE
|     type       | collectable |
+----------------+-------------+
|simple types    |             |
|string          |             |
|interned string |             |
|array           |      Y      |
|immutable array |             |
|object          |      Y      |
|resource        |             |
|reference       |             |

具體的垃圾回收過程這裡不再介紹。

以上就是解析PHP7核心之變數的內部實現的詳細內容,更多請關注TW511.COM其它相關文章!