在引擎內部,一個PHP的變量是保存在「zval」結構中,此結構包含了變量的類型和值信息,這個在以前的文章 變量的內部存儲:值和類型 中已經介紹了,此結構還有另外兩個字段信息,一個是"is_ref"(此字段在5.3.2版本中是is_ref__gc),此字段是一個布爾值,用來標識變量是不是一個引用,經過這個字段,PHP引擎可以區分通常的變量和引用變量。PHP代碼中能夠經過 & 操做符號來創建一個引用變量,創建的引用變量內部的zval的is_ref字段就爲1。zval中還有另一個字段refcount(此字段在5.3.2版本中是refcount__gc),這個字段是一個計數器,表示有多少個變量名指向這個zval容器,當此字段爲0時,表示沒有任何變量指向這個zval,那麼zval就能夠被釋放,這是引擎內部對內存的一種優化。考慮以下代碼:php
<!--?php $a = "Hello NowaMagic"; $b = $a; ?-->
代碼中有兩個變變量$a和$b,經過普通賦值方式將$a賦給$b,這樣$b的值和$a相等,對$b的修改不會對$a形成任何影響,那麼在這段代碼中,若是$a和$b對應兩個不一樣的zval,那麼顯然是對內存的一種浪費,PHP的開發者也不會讓這樣的事情發生。因此實際上$a和$b是指向同一個zval。這個zval的類型是STRING,值是"Hello world",有$a和$b兩個變量指向它,因此它的refcount=2, 因爲是一個普通賦值,因此is_ref字段爲0。 這樣就節省了內存開銷。函數
當執行$a = "Hello world"以後,$a對應的zval的信息爲:a: (refcount=1, is_ref=0)="Hello world"優化
但執行$b=$a以後,$a對應的zval的信息爲:a: (refcount=2, is_ref=0)="Hello world"spa
下面將以前的代碼修改一下:操作系統
<?php $a = "Hello world"; $b = &$a; ?>
這樣就經過引用賦值方式將$a賦給$b。.net
當執行$a = "Hello world"以後,$a對應的zval的信息爲:a: (refcount=1, is_ref=0)="Hello world"unix
但執行$b=&$a以後,$a對應的zval的信息爲:a: (refcount=2, is_ref=1)="Hello world"指針
能夠發現is_ref字段被設置成1了,這樣$a和$b對應的zval就是一個引用。這樣咱們基本對引擎中變量的引用和計數有了一個基本的瞭解,下面將介紹變量的分離。code
考慮前面第一段代碼,用普通方式將$a賦給$b,在內部兩個變量仍是指向同一個zval的,這個時候若是咱們將$b的值修改成"new string",$a變量的值依然是"Hello world":orm
<?php $a = "Hello world"; $b = $a; $b = "new string"; echo $a; echo $b; ?>
$a和$b明明是指向同一個zval,爲何修改了$b,$a還能保持不變呢,這就是copy on write(寫時複製)技術,簡單的說,當從新給$b賦值的時候,會將$b從以前的zval中分離出來。分離以後,$a和$b分別是指向不一樣的zval了。
寫時複製技術的一個比較有名的應用是在unix類操做系統內核中,當一個進程調用fork函數生成一個子進程的時候,父子進程擁有相同的地址空間內容,在老版本的系統中,子進程是在fork的時候就將父進程的地址空間中的內容都拷貝一份,對於規模較大的程序這個過程可能會有着很大的開銷,更崩潰的是,不少進程在fork以後,直接在子進程中調用exec執行另一個程序,這樣原來花了大量時間從父進程複製的地址空間都還沒來得及碰一下就被新的進程地址空間代替,這顯然是對資源的極大浪費,因此在後來的系統中,就使用了寫時複製技術,fork以後,子進程的地址空間仍是簡單的指向父進程的地址空間,只有當子進程須要寫地址空間中的內容的時候,纔會單獨分離一份(通常之內存頁爲單位)給子進程,這樣就算子進程立刻調用exec函數也不要緊,由於根本就不須要從父進程的地址空間中拷貝內容,這樣節約了內存同時又提升了速度。
當$b從$a指向的zval分離出來以後,zval的refcount就要減1,這樣由以前的2變成了1,表示這個zval還有一個變量指向它,就是$a。$b變量指向了一個新的zval,新的zval的refcount爲1,值爲字符串"new string",大概過程以下:
$a = "Hello world" //a: (refcount=1, is_ref=0)="Hello world" $b = $a //a,b: (refcount=2, is_ref=0)="Hello world" $b = "new string" //a: (refcount=1, is_ref=0)="Hello world" b: (refcount=1, is_ref=0)="new string"(發生分離操做)
這個分離邏輯能夠表敘爲:對一個通常變量a(isref=0)進行通常賦值操做,若是a所指向的zval的計數refcount大於1,那麼須要爲a從新分配一個新的zval,而且把以前的zval的計數refcount減小1。
以上爲普通賦值的狀況,若是是引用賦值,咱們看看這個變化過程:
$a = "Hello world" //a: (refcount=1, is_ref=0)="Hello world" $b = &$a //a,b: (refcount=2, is_ref=1)="Hello world" $b = "new string" //a,b: (refcount=2, is_ref=1)="new string"
能夠看出來,對一個引用類型的zval進行賦值是不會進行分離操做的,實際上咱們再產生一個引用變量的時候是可能出現一個分離操做的,只是時機有些不一樣:
在普通賦值的狀況下,分離操做發生在$b="new string"這一步,也就是在對變量賦新的值的時候,纔會進行zval分離操做
在引用賦值的狀況下,分離操做有可能發生在$b = &$a這一步,也就是在生成引用變量的時候
狀況1就很少解釋了,狀況2中強調是有可能發生分離,之前面的這代碼爲例子,是否進行分離與$a當前指向的zval的refcount有關係,代碼中$b = &$a 的時候, $a指向的zval的refcount=1,這個時候不須要進行分離操做,可是若是refcount=2,那麼就須要分離一個zval出來。好比以下代碼:
<?php $a = "Hello world"; $c = $a; $b = &$a; $b = "new string"; ?>
在執行引用賦值的時候,$a指向的zval的refcount=2,由於$a和$c同時指向了這個zval,因此在$b=&$a的時候,就須要進行一個分離操做,這個分離操做生成了一個ref=1的zval,而且計數爲2,由於$a,$b兩個變量指向分離出來的zval,原來的zval的refcount減小1,因此最終只有$c指向一個值爲"Hello world",ref=0的zval1, $a和$b指向一個值爲"Hello world",ref=1的zval2。 這樣咱們對$c的修改時在操做zval1,對$a和$b的修改都是在操做zval2,這樣就符合引用的特性了。
此過程大體以下:
$a = "Hello world"; //a: (refcount=1, is_ref=0)="Hello world" $c = $a; // a,c: (refcount=2, is_ref=0)="Hello world" $b = &$a; // c: (refcount=1, is_ref=0)="Hello world" a,b: (refcount=2, is_ref=1)="Hello world" (發生分離操做) $b = "new string"; // c: (refcount=1, is_ref=0)="Hello world" a,b: (refcount=2, is_ref=1)="new string"
試想一下若是不進行這個分離會有什麼後果?若是不進行分離,$a,$b,$c都指向了同一個zval,對$b的修改也會影響到$c,這顯然是不符合PHP語言特性的。
這個分離邏輯能夠表述爲:將一個通常變量a(isref=0)的引用賦給另一個變量b的時候,若是a的refcount大於1,那麼須要對a進行一次分離操做,分離以後的zval的isref等於1,refcount等於2
經過以上的一些知識和分離邏輯讀者應該能夠很容易分析其它的一些狀況。好比將一個引用變量a(isref=1)的引用賦給通常變量b的時候,須要將b以前指向的zval的refcount減小1,而後將b指向a的zval,a的zval的refcount加1,沒有任何分離操做
這些理論結合實際代碼會讓你更容易理解這個過程。
unset()並不是一個函數,而是一種語言結構,這個能夠經過查看編譯生成的opcode看到區別,unset對應的不是一個函數調用的opcode。那麼unset到底作了什麼? 在unset對應的opcode的handler中能夠看到相關內容,主要的操做時從當前符號表中刪除參數中的符號,好比在全局代碼中執行unset($a),那麼將會在全局符號表中刪除a這個符號。全局符號表是一張哈希表,創建這張表的時候會提供一個表中的項的析構函數,當咱們從符號表中刪除a的時候,會對符號a指向的項(這裏是zval的指針)調用這個析構函數,這個析構函數的主要功能是將a對應的zval的refcount減1,若是refcount變成了0,那麼釋放這個zval。因此當咱們調用unset的時候,不必定能釋放變量所佔的內存空間,只有當這個變量對應的zval沒有別的變量指向它的時候,纔會釋放掉zval,不然只是對refcount進行減1操做。