89-变量的引用与计数规则

原创 2016年04月28日 22:01:37

89-变量的引用与计数规则

变量的内部引用和计数

在引擎内部,一个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就可以被释放,这是引擎内部对内存的一种优化。考虑如下代码:

代码中有两个变变量ab,通过普通赋值方式将ab,这样ba相等,对ba造成任何影响,那么在这段代码中,如果ab对应两个不同的zval,那么显然是对内存的一种浪费,PHP的开发者也不会让这样的事情发生。所以实际上ab是指向同一个zval。这个zval的类型是STRING,值是”Hello world”,有ab两个变量指向它,所以它的refcount=2, 由于是一个普通赋值,所以is_ref字段为0。 这样就节省了内存开销。

当执行a="Helloworld"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;  
?>  

这样就通过引用赋值方式将ab。

当执行a="Helloworld"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了,这样ab对应的zval就是一个引用。这样我们基本对引擎中变量的引用和计数有了一个基本的了解,下面将介绍变量的分离。

变量的分离 copy on write

考虑前面第一段代码,用普通方式将ab,在内部两个变量还是指向同一个zval的,这个时候如果我们将b"newstring",a变量的值依然是”Hello world”:

<?php  
$a = "Hello world";  
$b = $a;  
$b = "new string";  
echo $a;  
echo $b;  
?>  

ab明明是指向同一个zval,为什么修改了b,a还能保持不变呢,这就是copy on write(写时复制)技术,简单的说,当重新给bb从之前的zval中分离出来。分离之后,ab分别是指向不同的zval了。

写时复制技术的一个比较有名的应用是在unix类操作系统内核中,当一个进程调用fork函数生成一个子进程的时候,父子进程拥有相同的地址空间内容,在老版本的系统中,子进程是在fork的时候就将父进程的地址空间中的内容都拷贝一份,对于规模较大的程序这个过程可能会有着很大的开销,更崩溃的是,很多进程在fork之后,直接在子进程中调用exec执行另外一个程序,这样原来花了大量时间从父进程复制的地址空间都还没来得及碰一下就被新的进程地址空间代替,这显然是对资源的极大浪费,所以在后来的系统中,就使用了写时复制技术,fork之后,子进程的地址空间还是简单的指向父进程的地址空间,只有当子进程需要写地址空间中的内容的时候,才会单独分离一份(一般以内存页为单位)给子进程,这样就算子进程马上调用exec函数也没关系,因为根本就不需要从父进程的地址空间中拷贝内容,这样节约了内存同时又提高了速度。

ba指向的zval分离出来之后,zval的refcount就要减1,这样由之前的2变成了1,表示这个zval还有一个变量指向它,就是ab变量指向了一个新的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进行赋值是不会进行分离操作的,实际上我们再产生一个引用变量的时候是可能出现一个分离操作的,只是时机有些不同:

  1. 在普通赋值的情况下,分离操作发生在$b=”new string”这一步,也就是在对变量赋新的值的时候,才会进行zval分离操作
  2. 在引用赋值的情况下,分离操作有可能发生在b = &a这一步,也就是在生成引用变量的时候

情况1就不多解释了,情况2中强调是有可能发生分离,以前面的这代码为例子,是否进行分离与azvalrefcountb = &a,a指向的zval的refcount=1,这个时候不需要进行分离操作,但是如果refcount=2,那么就需要分离一个zval出来。比如如下代码:

<?php  
$a = "Hello world";  
$c = $a;  
$b = &$a;  
$b = "new string";  
?>  

在执行引用赋值的时候,azvalrefcount=2,a和czval,b=&aref=1zval,2a,bzval,zvalrefcount1c指向一个值为”Hello world”,ref=0的zval1, ab指向一个值为”Hello world”,ref=1的zval2。 这样我们对czval1,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,czval,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操作。

opencv Mat引用计数详解

opencv提供两种复制的方式:深拷贝和浅拷贝 在发生拷贝构造函数和operator=函数时候,采用的是浅拷贝 copyto()和clone()函数发生的是深拷贝那么浅拷贝是如何实现的呢?浅拷贝发...
  • historycomputer
  • historycomputer
  • 2016年10月12日 09:47
  • 954

返回临时变量的引用

#include   #include   using   namespace   std   ; double&   Sum(int   iNum) { int   iB...
  • shulouxifeng
  • shulouxifeng
  • 2012年05月07日 10:22
  • 443

变量初始化规则以及声明和定义

当我们定义没有初始化式的变量的时候,系统有时候会帮我们初始化变量.系统提供什么样的值取决于变量的类型,也取决于变量定义的位置.内置类型变量的初始化.内置类型变量是否自动初始化取决于变量定义的位置.在函...
  • cool_mirror
  • cool_mirror
  • 2007年07月08日 16:53
  • 3232

数据的表示:常用的进制计数

1. 十进制:      Q:为什么我们从小学习的是十进制而不是其他进制?           A:  1)  我们的双手有10根手指,数满10个数再增加一双手就产生了十进制;         ...
  • xiexie4827
  • xiexie4827
  • 2016年06月22日 15:35
  • 307

[C++]引用参数与临时变量

对于引用参数,什么情况下会产生临时变量了?C++为什么对于产生临时变量的引用参数要加const?...
  • Jamienstar
  • Jamienstar
  • 2016年12月27日 18:22
  • 893

对临时变量的引用

以下代码来自《Imperfect C++》 #include stdio.h>int main()...{    long l = 2222;    short const &s = l;    l ...
  • jq0123
  • jq0123
  • 2008年01月10日 11:32
  • 1225

Java: 变量的值和引用

广义的说,在 Java 中,凡是可以指向一个对象或包含一个值的标识符,都可以称为变量。变量的内容,有可能是一个值,也有可能是指向一个对象的引用。当变量的类型为基本型别(short, byte, int...
  • YidingHe
  • YidingHe
  • 2009年05月21日 10:31
  • 5393

指针变量和引用变量的区别

关于reference的一些看法
  • forlove_you
  • forlove_you
  • 2016年03月26日 14:01
  • 532

js变量的规则

我们为了区分盒子,可以用BOX1,BOX2等名称代表不同盒子,BOX1就是盒子的名字(也就是变量的名字)。 我们赶快给变量取个好名字吧!变量名字可以任意取,只不过取名字要遵循一些规则: ...
  • JamesLi6
  • JamesLi6
  • 2016年06月17日 14:00
  • 744

C语言学习笔记(一)变量声明的规则

声明是告诉编译器有这么个变量,但并不实现。定义就是实现这个变量,真正在内存(堆或栈中)为此变量分配空间 它们的本质区别是:是否分配内存空间,定义需要分配空间,声明不需要分配空间。 int ...
  • nightduke1
  • nightduke1
  • 2013年11月23日 19:26
  • 1901
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:89-变量的引用与计数规则
举报原因:
原因补充:

(最多只允许输入30个字)