您的位置:首页 > 编程语言 > PHP开发

PHP内核探索:变量的引用与计数规则

2011-06-15 00:00 1991 查看

变量的内部引用和计数

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



代码中有两个变变量$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操作。

延伸阅读

此文章所在专题列表如下:

PHP内核探索:从SAPI接口开始
PHP内核探索:一次请求的开始与结束
PHP内核探索:一次请求生命周期
PHP内核探索:单进程SAPI生命周期
PHP内核探索:多进程/线程的SAPI生命周期
PHP内核探索:Zend引擎
PHP内核探索:再次探讨SAPI
PHP内核探索:Apache模块介绍
PHP内核探索:通过mod_php5支持PHP
PHP内核探索:Apache运行与钩子函数
PHP内核探索:嵌入式PHP
PHP内核探索:PHP的FastCGI
PHP内核探索:如何执行PHP脚本
PHP内核探索:PHP脚本的执行细节
PHP内核探索:操作码OpCode
PHP内核探索:PHP里的opcode
PHP内核探索:解释器的执行过程
PHP内核探索:变量概述
PHP内核探索:变量存储与类型
PHP内核探索:PHP中的哈希表
PHP内核探索:理解Zend里的哈希表
PHP内核探索:PHP哈希算法设计
PHP内核探索:翻译一篇HashTables文章
PHP内核探索:哈希碰撞攻击是什么?
PHP内核探索:常量的实现
PHP内核探索:变量的存储
PHP内核探索:变量的类型
PHP内核探索:变量的值操作
PHP内核探索:变量的创建
PHP内核探索:预定义变量
PHP内核探索:变量的检索
PHP内核探索:变量的类型转换
PHP内核探索:弱类型变量的实现
PHP内核探索:静态变量的实现
PHP内核探索:变量类型提示
PHP内核探索:变量的生命周期
PHP内核探索:变量赋值与销毁
PHP内核探索:变量作用域
PHP内核探索:诡异的变量名
PHP内核探索:变量的value和type存储
PHP内核探索:全局变量Global
PHP内核探索:变量类型的转换
PHP内核探索:内存管理开篇
PHP内核探索:Zend内存管理器
PHP内核探索:PHP的内存管理
PHP内核探索:内存的申请与销毁
PHP内核探索:引用计数与写时复制
PHP内核探索:PHP5.3的垃圾回收机制
PHP内核探索:内存管理中的cache
PHP内核探索:写时复制COW机制
PHP内核探索:数组与链表
PHP内核探索:使用哈希表API
PHP内核探索:数组操作
PHP内核探索:数组源码分析
PHP内核探索:函数的分类
PHP内核探索:函数的内部结构
PHP内核探索:函数结构转换
PHP内核探索:定义函数的过程
PHP内核探索:函数的参数
PHP内核探索:zend_parse_parameters函数
PHP内核探索:函数返回值
PHP内核探索:形参return value
PHP内核探索:函数调用与执行
PHP内核探索:引用与函数执行
PHP内核探索:匿名函数及闭包
PHP内核探索:面向对象开篇
PHP内核探索:类的结构和实现
PHP内核探索:类的成员变量
PHP内核探索:类的成员方法
PHP内核探索:类的原型zend_class_entry
PHP内核探索:类的定义
PHP内核探索:访问控制
PHP内核探索:继承,多态与抽象类
PHP内核探索:魔术函数与延迟绑定
PHP内核探索:保留类与特殊类
PHP内核探索:对象
PHP内核探索:创建对象实例
PHP内核探索:对象属性读写
PHP内核探索:命名空间
PHP内核探索:定义接口
PHP内核探索:继承与实现接口
PHP内核探索:资源resource类型
PHP内核探索:Zend虚拟机
PHP内核探索:虚拟机的词法解析
PHP内核探索:虚拟机的语法分析
PHP内核探索:中间代码opcode的执行
PHP内核探索:代码的加密与解密
PHP内核探索:zend_execute的具体执行过程
PHP内核探索:变量的引用与计数规则
PHP内核探索:新垃圾回收机制说明
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: