php5和7的對象賦值及zval粗談

今天看php發(fā)展歷史堤尾,了解到在php4的年代芋绸,對象的賦值默認(rèn)是創(chuàng)建副本媒殉,并非是增加對象的引用。就試了下在php5的環(huán)境下摔敛,怎樣將對象一般賦值廷蓉、引用賦值和拷貝。然后在sf里看到有人提了這么個問題:

class User
{
    public $name;
    public function __construct($name)
    {
        $this->name = $name;
    }
    public function __destruct()
    {
        echo 'I was released~~';
    }
}

$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;
$instance = null;
var_dump($instance);    // 輸出 null
var_dump($assignment);  // public 'name' => string 'xiao ming' (length=9)
var_dump($reference);   // 輸出 null

發(fā)現(xiàn)這個問題挺有意思马昙,就查了下php手冊桃犬,將引用和垃圾回收機制了解了下,才明白這些代碼背后發(fā)生了什么行楞。
先解釋下為什么$instance置為null后攒暇,$reference也變成null了。$reference是對$instance的引用子房,php官方文檔是這樣描述引用的:

References in PHP are a means to access the same variable content by different names. They are not like C pointers; for instance, you cannot perform pointer arithmetic using them, they are not actual memory addresses, and so on. Instead, they are symbol table aliases. Note that in PHP, variable name and variable content are different, so the same content can have different names.

從這段話我們就明白了形用,$instance存著指向小明對象的指針的值(這個后面詳細講)就轧,而$reference單純的只是$instance的另一個別名而已,并不像C那樣是實例的指針田度。因此$instance置為null妒御,釋放了對user對象的引用后,$reference也自然無法access the same variable了镇饺。
而$assignment還能同樣輸出小明這個對象乎莉,是因為$assignment存的內(nèi)容和$instance一樣,都是這個對象的內(nèi)存地址兰怠。這就又有一個問題了,這個小明對象在什么時候會被釋放呢李茫?

A PHP variable is stored in a container called a "zval". A zval container contains, besides the variable's type and value, two additional bits of information. The first is called "is_ref" and is a boolean value indicating whether or not the variable is part of a "reference set". This second piece of additional information, called "refcount", contains how many variable names (also called symbols) point to this one zval container.

這下我們明白了揭保,PHP使用了引用計數(shù)(reference count)垃圾回收機制。每個對象都內(nèi)含一個引用計數(shù)器魄宏,當(dāng)有reference連接到對象時秸侣,計數(shù)器加1。當(dāng)reference離開生存空間或被設(shè)為null時宠互,計數(shù)器減1味榛。當(dāng)某個對象的引用計數(shù)器為零時,PHP知道你將不再使用這個對象予跌,釋放其所占的內(nèi)存空間搏色。我們將上邊的代碼稍作修改,在$instance = null前加幾句話(ps:需要裝xdebug)

$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;

//php5輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  (refcount=2, is_ref=1)
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference');  // reference: (refcount=2, is_ref=1)
// php7輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  (refcount=2, is_ref=1)
xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0)
xdebug_debug_zval('reference');  // reference: (refcount=2, is_ref=1)

$instance = null;
$assignment = null;   // 這句話執(zhí)行后立即觸發(fā)User類的析構(gòu)函數(shù)券册,輸出 I was released~~

由于家里和公司的電腦環(huán)境一個是php5.6频轿,另個一是php7.1,同樣的demo中突然發(fā)現(xiàn)php5和php7里assignment的refcount值不一樣烁焙,就去看了下php對于變量的實現(xiàn)方式航邢,由于我的C功底實在有點差,源碼看的很吃力骄蝇,不足的地方還望各位能多多指教膳殷。先看下在php5和php7里對象賦值時的不同處理方式(簡化后的模型):


php5.6對象賦值

php7.1對象賦值

若在變量輸出前執(zhí)行unset($instance)和unset($reference),就更容易看出上圖所示不同之處了九火。

$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;

unset($instance);
//php5輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  no such symbol
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference');  // reference: (refcount=1, is_ref=0)
// php7輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  uninitialized
xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0)
xdebug_debug_zval('reference');  // reference: (refcount=1, is_ref=1)

unset($reference);
//php5輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  no such symbol
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference');  // reference: no such symbol
// php7輸出結(jié)果
xdebug_debug_zval('instance');   // instance:  uninitialized
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference');  // reference: uninitialized

// unset的作用僅僅是將變量從符號表中刪除赚窃,并減少所指結(jié)構(gòu)體的refcount值。

現(xiàn)在來詳細談下php5和php7里zval的不同之處岔激。
php7的zend_object如下:

struct _zend_object {
    zend_refcounted   gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

可以看出考榨,php7中的zval已經(jīng)變成了一個值指針,它要么保存著原始值鹦倚,要么保存著一個指針(索引)handle河质,這個索引對應(yīng)著一個保存原始對象的指針數(shù)組中某個元素的地址。也就是說,當(dāng)對象被創(chuàng)建時掀鹅,會有一個指針插入到對象存儲中并且其索引會保存在 handle 中散休,當(dāng)對象被釋放時,索引也會被移除乐尊。

那么php7為什么還要隔一層handle去訪問對象真正的內(nèi)容呢戚丸,而不是直接存著內(nèi)容的地址。The reason behind this is that during request shutdown, there comes a point where it is no longer safe to run userland code, because the executor is already partially shut down. To avoid this PHP will run all object destructors at an early point during shutdown and prevent them from running at a later point in time. For this a list of all active objects is needed.<摘自https://nikic.github.io/2015/06/19/Internal-value-representation-in-PHP-7-part-2.html扔嵌,作者是php官方開發(fā)組成員Nikita Popov>

并且 handle 對于調(diào)試也是很有用的限府,它讓每個對象都有了一個唯一的 ID,這樣就很容易區(qū)分兩個對象是同一個還是只是有相同的內(nèi)容痢缎。雖然 HHVM 沒有對象存儲的概念胁勺,但它也存了對象的 handle。


若將$assignment = $instance;改成$assignment = clone $instance;會發(fā)現(xiàn)assignment的ID將變?yōu)?

php5中取對象就比較麻煩了独旷。php5的zend_object_value結(jié)構(gòu)如下

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;

對象句柄(handler)是對象的唯一ID署穗,作為索引用于“對象存儲”,對象存儲本身是一個存儲容器(bucket)的數(shù)組嵌洼,bucket 定義如下:

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

第一個成員 object 是指向?qū)嶋H對象的指針案疲,現(xiàn)在看下object所指向的結(jié)構(gòu)體,對于普通用戶區(qū)所定義的object內(nèi)容通常如下:

typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards;
} zend_object;

現(xiàn)在我們要從對象 zval 中取出一個元素麻养,先需要取出the object store bucket褐啡,然后是 zend object,然后才能通過指針找到對象屬性表和 zval鳖昌。這樣這里至少就有 4 層間接訪問(并且實際使用中可能最少需要7層)春贸。

簡單點說,在php7中的zval遗遵,相當(dāng)于php5的zval*萍恕。只不過相對于zval*,直接存儲zval车要,我們可以省掉一次指針解引用允粤。

總結(jié)一下,php7新的zval設(shè)計不再單獨從堆上分配內(nèi)存并且不自己存儲引用計數(shù)翼岁。需要使用 zval 指針的復(fù)雜類型(比如字符串类垫、數(shù)組和對象)會自己存儲引用計數(shù)(php5除了zval本身有引用計數(shù)以外,bucket也存儲了refcount)琅坡。這樣就可以有更少的內(nèi)存分配悉患、更少的間接指針以及更少的內(nèi)存使用。

補充:
1榆俺、php7中簡單數(shù)據(jù)類型不再單獨分配內(nèi)存售躁,也不再計數(shù)坞淮。所以會有以下情況:

$a = 1;
// php5輸出結(jié)果
xdebug_debug_zval('a);  // (refcount=1, is_ref=0)

// php7輸出結(jié)果
xdebug_debug_zval('a);  // (refcount=0, is_ref=0)

2、php5中refcount等于1時陪捷,is_ref永遠等于0回窘;php7則不會


版權(quán)聲明:本文為原創(chuàng)文章,轉(zhuǎn)載請注明出處市袖,謝謝~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啡直,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子苍碟,更是在濱河造成了極大的恐慌酒觅,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件微峰,死亡現(xiàn)場離奇詭異舷丹,居然都是意外死亡,警方通過查閱死者的電腦和手機县忌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門掂榔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來继效,“玉大人症杏,你說我怎么就攤上這事∪鹦牛” “怎么了厉颤?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凡简。 經(jīng)常有香客問我逼友,道長,這世上最難降的妖魔是什么秤涩? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任帜乞,我火速辦了婚禮,結(jié)果婚禮上筐眷,老公的妹妹穿的比我還像新娘黎烈。我一直安慰自己,他們只是感情好匀谣,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布照棋。 她就那樣靜靜地躺著,像睡著了一般武翎。 火紅的嫁衣襯著肌膚如雪烈炭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天宝恶,我揣著相機與錄音符隙,去河邊找鬼趴捅。 笑死,一個胖子當(dāng)著我的面吹牛膏执,可吹牛的內(nèi)容都是我干的驻售。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼更米,長吁一口氣:“原來是場噩夢啊……” “哼欺栗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起征峦,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤迟几,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栏笆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體类腮,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年蛉加,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚜枢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡针饥,死狀恐怖厂抽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丁眼,我是刑警寧澤筷凤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站苞七,受9級特大地震影響藐守,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹂风,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一卢厂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧惠啄,春花似錦慎恒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姥闭,卻和暖如春丹鸿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棚品。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工靠欢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留廊敌,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓门怪,卻偏偏與公主長得像骡澈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掷空,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容