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操作。

相关文章推荐

COM的引用计数规则

“COM本质论”一书对COM的引用计数规则精简为以下3个公理:     (1)当一个非空的接口指针从一个内存位置被copy到另一个内存位置时,应该要调用AddRef,以便通知对象“又有附加的引用发生...

有效的使用和设计COM智能指针 ——条款16:智能指针的引入不能违反COM引用计数规则

条款16:智能指针的引入不能违反COM引用计数规则 更多条款请前往原文出处:http://blog.csdn.net/liuchang5 我们之前已经看过类似的函数。它是严格遵照了引用...

OC内存管理教程之ARC(二)——自动引用计数规则

前言:        上一篇介绍了Objective-C的内存管理,本篇讲述ARC所引起的变化。        实际上,“引用计数式的内存管理”的本质在ARC中并没有改变。就算开启了ARC,编译器关于...

Proteus仿真AT89C52——计数

  • 2017年03月14日 22:59
  • 374KB
  • 下载

基于STC89C52的串口计数

  • 2014年11月10日 16:34
  • 736B
  • 下载

more effective c++——Item M29 引用计数(二)带静态成员变量的rfstring类实现

more effective c++——Item M29 引用计数(一)简略的rfstring类设计和写时拷贝 这篇博客中所实现的引用计数还存在未解决的问题——如何通过相同的值多次构造rfstri...

STC89C52的中断计数

  • 2012年01月05日 14:18
  • 136KB
  • 下载

php深入学习之变量的引用计数

php变量的引用计数是什么呢?首先看一下php变量的底层结构:typedef struct _zval_struct zval; struct _zval_struct { zvalue_va...

COM 的引用计数规则 AddRef/Release 规则

COM 的引用计数规则可以精简为以下三个简单的公理: 1、当一个非空的接口指针从一个内存位置被拷贝到另一个内存位置之前,应该要调用 AddRef,以便通知对象又有附加的引用发生了。 2、对于已...
  • yockie
  • yockie
  • 2013年12月12日 21:17
  • 875
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:89-变量的引用与计数规则
举报原因:
原因补充:

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