文档章节

php变量的引用与计数规则

stone_
 stone_
发布于 2015/10/15 16:20
字数 2477
阅读 28
收藏 0
点赞 0
评论 0

变量的内部引用和计数

在引擎内部,一个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进行赋值是不会进行分离操作的,实际上我们再产生一个引用变量的时候是可能出现一个分离操作的,只是时机有些不同:

  1. 在普通赋值的情况下,分离操作发生在$b="new string"这一步,也就是在对变量赋新的值的时候,才会进行zval分离操作

  2. 在引用赋值的情况下,分离操作有可能发生在$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操作。


本文转载自:http://www.nowamagic.net/librarys/veda/detail/1581

共有 人打赏支持
stone_
粉丝 19
博文 221
码字总数 163611
作品 0
洛阳
程序员
PHP笔记 变量和内存管理

鸟哥的博客是个大宝库,正在从头看起。 《深入理解PHP内存管理之谁动了我的内存》 http://www.laruence.com/2011/03/04/1894.html 1、memorygetusage 函数 int memorygetusage ([ bool $real...

daweilang
2017/03/12
0
0
[译]变量在 PHP7 内部的实现(一)

http://0x1.im/blog/php/Internal-value-representation-in-PHP-7-part-1.html → About → Links → Github → 公众号 Scholer's Blog [译]变量在 PHP7 内部的实现一 Dec 10, 2015 本文第一......

污湖洞主
2017/06/11
0
0
PHP垃圾回收机制理解

使用的是“引用计数”方式进行回收。简单地理解的话,就是每个分配的内存区域都有一个计数器,记录有多少个变量指针指向这片内存。当指向该片内存的指针数量为0,那么该片内存区域就可以被回...

刘元兴
2017/05/18
0
0
zval_dtor与zval_ptr_dtor的区别

这两个东西长得很像,起初我还错误以为一个是针对zval一个针对zval*的释放函数,唉,太天真了。这两个函数都与zval的释放有关,是我们肯定会经常碰到的两个函数。 下面是两者的声明: //zval_...

mickelfeng
2013/05/01
0
0
PHP的内存泄露问题与垃圾回收

你写了一个php脚本,一般都不用考虑内存泄露和垃圾回收的问题,因为一般情况下你的脚本很快就执行完退出了。 但在一些运行时间长,数据量大的时候,程序运行一段时间后,php脚本就占用了过多...

Jean
2014/05/19
0
0
PHPer面试指南-PHP 篇

本书的 GitHub 地址:https://github.com/todayqq/PHPerInterviewGuide PHP 篇收集了一些常见的基础、进阶面试题,基础的面试题不再作答。 基础篇 Get 和 POST 的区别 Cookie 和 Session 的区...

angkee
01/24
0
0
从php代码分析php的GC(垃圾回收)机制

首先咱先看到例子,最简单不过的执行流程了: Example 1: gc.php <?php errorreporting(EALL); $a = 'I am test.'; $b = & $a; echo $b ."n"; ?> 不用说 % php -f gc.php 输出结果非常明了: h......

安安-Anan
2015/08/12
0
0
php垃圾回收

Example #1 生成一个新的zval容器 <?php $a = "new string"; ?> 在上例中,新的变量a,是在当前作用域中生成的。并且生成了类型为 string 和值为new string的变量容器。在额外的两个字节信息...

旋转木马-千里马
2016/01/06
39
0
PHP内核介绍及扩展开发指南—基础知识

一、 基础知识 本章简要介绍一些Zend引擎的内部机制,这些知识和Extensions密切相关,同时也可以帮助我们写出更加高效的PHP代码。 1.1 PHP变量的存储 1.1.1 zval结构 Zend使用zval结构来存储...

晨曦之光
2012/03/09
86
0
Objective-C高级编程之引用计数,看我就够了

自动引用计数.png 1.1 什么是自动引用计数 概念:在 LLVM 编译器中设置 ARC(Automaitc Reference Counting) 为有效状态,就无需再次键入 或 代码。 1.2 内存管理 / 引用计数 1.2.1 概要 引用...

shenglanya
2017/12/19
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Git 2.18版本发布:支持Git协议v2,提升性能

Git 2.18版本发布:支持Git协议v2,提升性能Git 2.18版本发布:支持Git协议v2,提升性能 新版本协议的主要驱动力是使 Git 服务端能够对各种 ref(分支与 tag)进行过滤操作。 这就意味着,G...

linux-tao
22分钟前
0
0
python浏览器自动化测试库【2018/7/22-更新】

64位py2.7版本 更新 document_GetResources 枚举页面资源 document_GetresourceText 获取指定url的内容 包括页面图片 下载地址下载地址 密码:upr47x...

开飞色
38分钟前
27
0
关于DCL双重锁失效及解决方案

关于DCL双重锁失效及解决方案 Double Check Lock (DCL)实现单例 DCL 方式实现单例的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance方法不进行...

DannyCoder
44分钟前
0
0
PowerDesigner 16.5 安装配置

PowerDesigner16.5破解版是一款业内领先且开发人员常用的数据库建模工具,PowerDesigner可以从物理和概念两个层面设计数据库,方便用户制作处清晰直观的数据流程图和结构模型,欢迎有需要的朋...

Gibbons
今天
0
0
mac Homebrew 指令积累

1通用命令 brew install [包名] //安装包 brew list //列举安装的包 brew info [包名] // 显示安装包的详细信息 mysql 相关 #启动mysql 服务 brew service start mysql my...

Kenny100120
今天
0
0
前端Tips: 创建, 发布自己的 Vue UI 组件库

创建, 发布自己的 Vue UI 组件库 前言 在使用 Vue 进行日常开发时, 我们经常会用到一些开源的 UI 库, 如: Element-UI, Vuetify 等. 只需一行命令, 即可方便的将这些库引入我们当前的项目: n...

ssthouse_hust
今天
1
0
大数据教程(2.13):keepalived+nginx(多主多活)高可用集群搭建教程【自动化脚本】

上一章节博主为大家介绍了目前大型互联网项目的keepalived+nginx(主备)高可用系统架构体系,相信大家应该看了博主的文章对keepalived/nginx技术已经有一定的了解,在本节博主将为大家分享k...

em_aaron
今天
5
0
Git 2.18版本发布:支持Git协议v2,提升性能

在最新的官方 Git 客户端正式版2.18中添加了对 Git wire 协议 v2 的支持,并引入了一些性能与 UI 改进的新特性。在 Git 的核心团队成员 Brandon Williams 公开宣布这一消息前几周,Git 协议 ...

六库科技
今天
0
0
Java8新特性之接口

在JDK8以前,我们定义接口类中,方法都是抽象的,并且不能存在静态方法。所有的方法命名规则基本上都是 public [返回类型] [方法名](参数params) throws [异常类型] {}。 JDK8为接口的定义带...

developlee的潇洒人生
今天
0
0
aop + annotation 实现统一日志记录

aop + annotation 实现统一日志记录 在开发中,我们可能需要记录异常日志。由于异常比较分散,每个 service 方法都可能发生异常,如果我们都去做处理,会出现很多重复编码,也不好维护。这种...

长安一梦
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部