php变量的引用与计数规则
為什么80%的碼農都做不了架構師?>>> ??
變量的內部引用和計數
在引擎內部,一個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?? $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"
下面將之前的代碼修改一下:
<?php?? $a?=?"Hello?world";?? $b?=?&$a;?? ?>這樣就通過引用賦值方式將$a賦給$b。
當執行$a = "Hello world"之后,$a對應的zval的信息為:a: (refcount=1, is_ref=0)="Hello world"
但執行$b=&$a之后,$a對應的zval的信息為:a: (refcount=2, is_ref=1)="Hello world"
可以發現is_ref字段被設置成1了,這樣$a和$b對應的zval就是一個引用。這樣我們基本對引擎中變量的引用和計數有了一個基本的了解,下面將介紹變量的分離。
變量的分離 copy on write
考慮前面第一段代碼,用普通方式將$a賦給$b,在內部兩個變量還是指向同一個zval的,這個時候如果我們將$b的值修改為"new string",$a變量的值依然是"Hello world":
<?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的作用
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操作。
轉載于:https://my.oschina.net/shyl/blog/517656
總結
以上是生活随笔為你收集整理的php变量的引用与计数规则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用C#.NET调用Java开发的WebS
- 下一篇: FMDB/SQLCipher数据库管理