PHP內核之zval

原文地址
做者:Twei 主頁php

前言


以前面試的時候面試官問過php中變量是如何實現的,遺憾的是隻答道了大概是用結構體實現的。這篇文章是谷歌以後以爲總結 的比較到位的,故轉載進而學習之。html

正文


PHP中的數據類型node


相對於 C、 C++、 Java等其餘編程語言,PHP 是一個弱類型的語言,意味着當咱們要使用一個變量時,不須要去聲明它的類型。這個特性給咱們帶來了不少便利,同時有時也會帶來一些陷阱。那麼,PHP 是真的沒有數據類型這個說法嗎?git

固然不是。在 PHP 官方文檔中將 PHP 中的變量劃分爲三類:標量類型、複雜類型和特殊類型。標量類型包括布爾型(bool)、整型(int)、浮點型(float)和字符串(string);複雜類型包括數組(array)和對象(object);特殊類型包括 NULL 和資源(resource)。因此說 PHP 的變量細分的話,有 8 種數據類型。github

總所周知,PHP 的底層是用 C 語言實現的。咱們的 PHP 腳本會通過 Zend 引擎解析爲 C 代碼再執行。那麼,一個 PHP 的變量,在 C 語言上是怎麼表示的呢?它最終會被解析成什麼樣呢?web

答案就是 zval。 無論什麼類型的 PHP 變量,在 PHP 源代碼中統一用一個叫作 zval 的結構表示。 zval 能夠看作是 PHP 變量在 C 代碼中的容器,它存儲了這個變量的值、類型等相關信息。面試

那麼咱們就看一下 zval 的基本結構(須要一點 C 語言的基本知識)。算法

zval的基本結構編程


在 PHP 源代碼中 zval 這個結構是一個名叫 _zval_struct 的結構體(struct),具體定義在源代碼的 Zend/zend.h 文件中,下面是相關代碼的摘錄:數組

struct _zval_struct {
    zvalue_value value;       /* value */ 
    zend_uint refcount__gc;   /* value of ref count */
    zend_uchar type; /* active type */ 
    zend_uchar is_ref__gc;    /* if it is a ref variable */ 
}; 
typedef struct _zval_struct zval;

也就是說,在 PHP 的源碼中,就用這一個結構體表示 PHP 中各類類型的變量,而且還能夠實現其餘的一些功能,例如垃圾回收(GC:Grabage Collection)。

能夠看到它由 4 個字段構成,分別表示這個變量的某個信息。

ZVALUE_VALUE VALUE


value 用來表示變量的實際值,具體來講它是一個 zvalue_value 的聯合體(union):

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {                    /* string */
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value,used for array */
    zend_object_value obj;      /* object */
} zvalue_value;

能夠看到 _zvalue_value 中只有 5 個字段,可是 PHP 中有 8 種數據類型,那麼如何用 5 個字段表示 8 種類型呢?

這算是 PHP 設計比較巧妙的一個地方,它經過複用字段達到了減小字段的目的。例如,在 PHP 內部布爾型、整型及資源(只要存儲資源的標識符便可)都是經過 lval 字段存儲的;dval 用於存儲浮點型;str 存儲字符串;ht 存儲數組(注意 PHP 中的數組實際上是哈希表);而 obj 存儲對象類型;若是全部字段所有置爲 0 或 NULL則表示 PHP 中的 NULL,這樣就達到了用 5 個字段存儲 8 種類型的值。

ZEND_UINT REFCOUNT__GC


從它的後綴 gc 能夠看到,這個字段是和垃圾回收相關的。

它其實是一個計數器,用來保存有多少變量指向該zval。在變量生成時,置爲1,也就是 refcount = 1。

對變量進行不一樣的操做會改變它的值。典型的賦值操做如 a = b 會使 refcount 加 1,而 unset() 操做會相應的減 1。

經過判斷它的值能夠進行垃圾回收。在 PHP5.3 以前,使用引用計數的機制來實現 GC:若是一個 zval 的 refcount 減爲 0,那麼 Zend 引擎會認爲沒有任何變量指向該 zval,就會釋放該 zval 所佔的內存空間。但僅僅使用引用計數機制沒法釋放掉循環引用的 zval,這是就會致使內存泄露(Memory Leak)。

在 5.3 之前,這個字段的名字還叫作 refcount,5.3 之後,在引入新的垃圾回收算法來對付循環引用,做者加入了大量的宏來操做 refcount,爲了能讓錯誤更快的顯現,因此更名爲 refcount__gc, 迫使你們都使用宏來操做 refcount。

相似的, 還有第四個字段 is_ref, 這個值表示了 PHP 中的一個類型是不是引用。
想了解 PHP 的垃圾回收機制,能夠參考這篇博客:PHP的垃圾回收機制
注:變量,也能夠被稱爲符號,symbol。全部的符號都存在符號表(symbol table)中, 不一樣的做用域使用不一樣的符號表,關於這一點,這篇博客進行了講解。

ZEND_UCHAR TYPE


這個字段用於代表變量屬於 PHP 8 種類型的哪一種。在 zend 內部,這些類型對應於下面的宏(代碼位置 phpsrc/Zend/zend.h):

#define IS_NULL 0
#define IS_LONG 1
#define IS_DOUBLE 2
#define IS_BOOL 3
#define IS_ARRAY 4
#define IS_OBJECT 5
#define IS_STRING 6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_ARRAY 9
#define IS_CALLABLE 10

ZEND_UCHAR IS_REF__GC


這個字段用於標記變量是不是引用變量。對於普通的變量,該值爲 0,而對於引用型的變量,該值爲 1。這個變量會影響 zval 的共享、分離等。它也和 PHP 的垃圾回收有關。

PHP7中的zval


上述的 zval 結構,隨着時間的發展,暴露出許多問題,例如佔用空間大(24 字節)、不支持拓展、 對象和引用效率差等,因此在 PHP7 的時候,對 zval 進行了較大的改變,如今它的結構是這樣的:

struct _zval_struct {
    union {
        zend_long         lval;             /* long value */
        double            dval;             /* double value */
        zend_refcounted  *counted;
        zend_string      *str;
        zend_array       *arr;
        zend_object      *obj;
        zend_resource    *res;
        zend_reference   *ref;
        zend_ast_ref     *ast;
        zval             *zv;
        void             *ptr;
        zend_class_entry *ce;
        zend_function    *func;
        struct {
            uint32_t w1;
            uint32_t w2;
        } ww;
    } value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 /* hash collision chain */
        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 在 64 位環境下,只須要 16 個字節(2 個指針 size)。PHP7 中的 zval,已經變成了一個值指針,它要麼保存着原始值,要麼保存着指向一個保存原始值的指針。

這部份內容來自鳥哥的GitHub

總結


  1. zval 是一種 C 語言實現的數據結構,功能是做爲 PHP 變量的容器;
  2. 它保存了變量的各類信息(如類型和值),併爲其餘功能(如垃圾回收)提供支持;
  3. 在不一樣的 PHP 版本中,它的結構不一樣。PHP7 的 zval 佔 16 個字節,PHP5 的要佔 24 個字節。

參考


PHP內核探索之變量(1)變量的容器-Zval
PHP垃圾回收深刻理解
深刻理解PHP7之zval