## 2.1 變量的內部實現
變量是一個語言實現的基礎,變量有兩個組成部分:變量名、變量值,PHP中可以將其對應為:zval、zend_value,這兩個概念一定要區分開,PHP中變量的內存是通過引用計數進行管理的,而且PHP7中引用計數是在zend_value而不是zval上,變量之間的傳遞、賦值通常也是針對zend_value。
PHP中可以通過`$`關鍵詞定義一個變量:`$a;`,在定義的同時可以進行初始化:`$a = "hi~";`,注意這實際是兩步:定義、初始化,只定義一個變量也是可以的,可以不給它賦值,比如:
```php
$a;
$b = 1;
```
這段代碼在執行時會分配兩個zval。
接下來我們具體看下變量的結構以及不同類型的實現。
### 2.1.1 變量的基礎結構
```c
//zend_types.h
typedef struct _zval_struct zval;
typedef union _zend_value {
zend_long lval; //int整形
double dval; //浮點型
zend_refcounted *counted;
zend_string *str; //string字符串
zend_array *arr; //array數組
zend_object *obj; //object對象
zend_resource *res; //resource資源類型
zend_reference *ref; //引用類型,通過&$var_name定義的
zend_ast_ref *ast; //下面幾個都是內核使用的value
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
struct _zval_struct {
zend_value value; //變量實際的value
union {
struct {
ZEND_ENDIAN_LOHI_4( //這個是為了兼容大小字節序,小字節序就是下面的順序,大字節序則下面4個順序翻轉
zend_uchar type, //變量類型
zend_uchar type_flags, //類型掩碼,不同的類型會有不同的幾種屬性,內存管理會用到
zend_uchar const_flags,
zend_uchar reserved) //call info,zend執行流程會用到
} v;
uint32_t type_info; //上面4個值的組合值,可以直接根據type_info取到4個對應位置的值
} u1;
union {
uint32_t var_flags;
uint32_t next; //哈希表中解決哈希沖突時用到
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
} u2; //一些輔助值
};
```
`zval`結構比較簡單,內嵌一個union類型的`zend_value`保存具體變量類型的值或指針,`zval`中還有兩個union:`u1`、`u2`:
* __u1:__ 它的意義比較直觀,變量的類型就通過`u1.v.type`區分,另外一個值`type_flags`為類型掩碼,在變量的內存管理、gc機制中會用到,第三部分會詳細分析,至于后面兩個`const_flags`、`reserved`暫且不管
* __u2:__ 這個值純粹是個輔助值,假如`zval`只有:`value`、`u1`兩個值,整個zval的大小也會對齊到16byte,既然不管有沒有u2大小都是16byte,把多余的4byte拿出來用于一些特殊用途還是很劃算的,比如next在哈希表解決哈希沖突時會用到,還有fe_pos在foreach會用到......
從`zend_value`可以看出,除`long`、`double`類型直接存儲值外,其它類型都為指針,指向各自的結構。
### 2.1.2 類型
`zval.u1.type`類型:
```c
/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
/* constant expressions */
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
```
#### 2.1.2.1 標量類型
最簡單的類型是true、false、long、double、null,其中true、false、null沒有value,直接根據type區分,而long、double的值則直接存在value中:zend_long、double,也就是標量類型不需要額外的value指針。
#### 2.1.2.2 字符串
PHP中字符串通過`zend_string`表示:
```c
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
```
* __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,后面用到的時候再具體分析。
#### 2.1.2.3 數組
array是PHP中非常強大的一個數據結構,它的底層實現就是普通的有序HashTable,這里簡單看下它的結構,下一節會單獨分析數組的實現。
```c
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc; //引用計數信息,與字符串相同
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar reserve)
} v;
uint32_t flags;
} u;
uint32_t nTableMask; //計算bucket索引時的掩碼
Bucket *arData; //bucket數組
uint32_t nNumUsed; //已用bucket數
uint32_t nNumOfElements; //已有元素數,nNumOfElements <= nNumUsed,因為刪除的并不是直接從arData中移除
uint32_t nTableSize; //數組的大小,為2^n
uint32_t nInternalPointer; //數值索引
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
```
#### 2.1.2.4 對象/資源
```c
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle;
zend_class_entry *ce; //對象對應的class類
const zend_object_handlers *handlers;
HashTable *properties; //對象屬性哈希表
zval properties_table[1];
};
struct _zend_resource {
zend_refcounted_h gc;
int handle;
int type;
void *ptr;
};
```
對象比較常見,資源指的是tcp連接、文件句柄等等類型,這種類型比較靈活,可以隨意定義struct,通過ptr指向,后面會單獨分析這種類型,這里不再多說。
#### 2.1.2.5 引用
引用是PHP中比較特殊的一種類型,它實際是指向另外一個PHP變量,對它的修改會直接改動實際指向的zval,可以簡單的理解為C中的指針,在PHP中通過`&`操作符產生一個引用變量,也就是說不管以前的類型是什么,`&`首先會創建一個`zend_reference`結構,其內嵌了一個zval,這個zval的value指向原來zval的value(如果是布爾、整形、浮點則直接復制原來的值),然后將原zval的類型修改為IS_REFERENCE,原zval的value指向新創建的`zend_reference`結構。
```c
struct _zend_reference {
zend_refcounted_h gc;
zval val;
};
```
結構非常簡單,除了公共部分`zend_refcounted_h`外只有一個`val`,舉個示例看下具體的結構關系:
```php
$a = "time:" . time(); //$a -> zend_string_1(refcount=1)
$b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
```
最終的結果如圖:

注意:引用只能通過`&`產生,無法通過賦值傳遞,比如:
```php
$a = "time:" . time(); //$a -> zend_string_1(refcount=1)
$b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = $b; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2)
//$c -> ---
```
`$b = &$a`這時候`$a`、`$b`的類型是引用,但是`$c = $b`并不會直接將`$b`賦值給`$c`,而是把`$b`實際指向的zval賦值給`$c`,如果想要`$c`也是一個引用則需要這么操作:
```php
$a = "time:" . time(); //$a -> zend_string_1(refcount=1)
$b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = &$b;/*或$c = &$a*/ //$a,$b,$c -> zend_reference_1(refcount=3) -> zend_string_1(refcount=1)
```
這個也表示PHP中的 __引用只可能有一層__ ,__不會出現一個引用指向另外一個引用的情況__ ,也就是沒有C語言中`指針的指針`的概念。
### 2.1.3 內存管理
接下來分析下變量的分配、銷毀。
在分析變量內存管理之前我們先自己想一下可能的實現方案,最簡單的處理方式:定義變量時alloc一個zval及對應的value結構(ref/arr/str/res...),賦值、函數傳參時硬拷貝一個副本,這樣各變量最終的值完全都是獨立的,不會出現多個變量同時共用一個value的情況,在執行完以后直接將各變量及value結構free掉。
這種方式是可行的,而且內存管理也很簡單,但是,硬拷貝帶來的一個問題是效率低,比如我們定義了一個變量然后賦值給另外一個變量,可能后面都只是只讀操作,假如硬拷貝的話就會有多余的一份數據,這個問題的解決方案是: __引用計數+寫時復制__ 。PHP變量的管理正是基于這兩點實現的。
#### 2.1.3.1 引用計數
引用計數是指在value中增加一個字段`refcount`記錄指向當前value的數量,變量復制、函數傳參時并不直接硬拷貝一份value數據,而是將`refcount++`,變量銷毀時將`refcount--`,等到`refcount`減為0時表示已經沒有變量引用這個value,將它銷毀即可。
```php
$a = "time:" . time(); //$a -> zend_string_1(refcount=1)
$b = $a; //$a,$b -> zend_string_1(refcount=2)
$c = $b; //$a,$b,$c -> zend_string_1(refcount=3)
unset($b); //$b = IS_UNDEF $a,$c -> zend_string_1(refcount=2)
```
引用計數的信息位于給具體value結構的gc中:
```c
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
```
從上面的zend_value結構可以看出并不是所有的數據類型都會用到引用計數,`long`、`double`直接都是硬拷貝,只有value是指針的那幾種類型才__可能__會用到引用計數。
下面再看一個例子:
```php
$a = "hi~";
$b = $a;
```
猜測一下變量`$a/$b`的引用情況。
這個不跟上面的例子一樣嗎?字符串`"hi~"`有`$a/$b`兩個引用,所以`zend_string1(refcount=2)`。但是這是錯的,gdb調試發現上面例子zend_string的引用計數為0。這是為什么呢?
```c
$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_STRING`、`IS_ARRAY`,與普通string、array類型相同,那怎么區分一個value是否支持引用計數呢?還記得`zval.u1`中那個類型掩碼`type_flag`嗎?正是通過這個字段標識的,這個字段除了標識value是否支持引用計數外還有其它幾個標識位,按位分割,注意:`type_flag`與`zval.value->gc.u.flag`不是一個值。
支持引用計數的value類型其`zval.u1.type_flag` __包含__ (注意是&,不是等于)`IS_TYPE_REFCOUNTED`:
```c
#define IS_TYPE_REFCOUNTED (1<<2)
```
下面具體列下哪些類型會有這個標識:
```c
| 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的時候才會用到這種類型,不清楚具體實現,暫時忽略。
#### 2.1.3.2 寫時復制
上一小節介紹了引用計數,多個變量可能指向同一個value,然后通過refcount統計引用數,這時候如果其中一個變量試圖更改value的內容則會重新拷貝一份value修改,同時斷開舊的指向,寫時復制的機制在計算機系統中有非常廣的應用,它只有在必要的時候(寫)才會發生硬拷貝,可以很好的提高效率,下面從示例看下:
```php
$a = array(1,2);
$b = &$a;
$c = $a;
//發生分離
$b[] = 3;
```
最終的結果:

不是所有類型都可以copy的,比如對象、資源,事實上只有string、array兩種支持,與引用計數相同,也是通過`zval.u1.type_flag`標識value是否可復制的:
```c
#define IS_TYPE_COPYABLE (1<<4)
```
```c
| 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`的屬性就行了。
#### 2.1.3.3 變量回收
PHP變量的回收主要有兩種:主動銷毀、自動銷毀。主動銷毀指的就是 __unset__ ,而自動銷毀就是PHP的自動管理機制,在return時減掉局部變量的refcount,即使沒有顯式的return,PHP也會自動給加上這個操作,另外一個就是寫時復制時會斷開原來value的指向,這時候也會檢查斷開后舊value的refcount。
#### 2.1.3.4 垃圾回收
PHP變量的回收是根據refcount實現的,當unset、return時會將變量的引用計數減掉,如果refcount減到0則直接釋放value,這是變量的簡單gc過程,但是實際過程中出現gc無法回收導致內存泄漏的bug,先看下一個例子:
```php
$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`區分的:
```c
#define IS_TYPE_COLLECTABLE
```
```c
| type | collectable |
+----------------+-------------+
|simple types | |
|string | |
|interned string | |
|array | Y |
|immutable array | |
|object | Y |
|resource | |
|reference | |
```
具體的垃圾回收過程這里不再介紹,后面會單獨分析。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述