摘要:引用计数变量分离写时拷贝我们一步步来理解语言特性是脚本语言,所谓脚本语言,就是说并不是独立运行的,要运行代码需要解析器,用户编写的代码最终都会被解析器解析执行的执行是通过引擎,是用编写的用户编写的代码最终都会被翻译成的虚拟机的虚拟指令来执行
zval、引用计数、变量分离、写时拷贝
我们一步步来理解
1、php语言特性
PHP是脚本语言,所谓脚本语言,就是说PHP并不是独立运行的,要运行PHP代码需要PHP解析器,用户编写的PHP代码最终都会被PHP解析器解析执行
PHP的执行是通过Zend engine(ZE, Zend引擎),ZE是用C编写的
用户编写的PHP代码最终都会被翻译成PHP的虚拟机ZE的虚拟指令(OPCODES)来执行
也就说最终会被翻译成一条条的指令
既然这样,有什么结果和你预想的不一样,查看php源码是最直接最有效的
2、php变量的存储结构
在PHP中,所有的变量都是用一个结构zval结构来保存的,在Zend/zend.h中可以看到zval的定义:
zval结构包括:
① value —— 值,是真正保存数据的关键部分,定义为一个联合体(union)
② type —— 用来储存变量的类型
③ is_ref —— 下面介绍
④ refcount —— 下面介绍
声明一个变量
$addr="北京";
PHP内部都是使用zval来表示变量的,那对于上面的脚本,ZE是如何把addr和内部的zval结构联系起来的呢?
变量都是有名字的(本例中变量名为addr)
而zval中并没有相应的字段来体现变量名。PHP内部肯定有一个机制,来实现变量名到zval的映射
在PHP中,所有的变量都会存储在一个数组中(确切的说是hash table)
当你创建一个变量的时候,PHP会为这个变量分配一个zval,填入相应的信息,然后将这个变量的名字和指向这个zval的指针填入一个数组中。当你获取这个变量的时候,PHP会通过查找这个数组,取得对应的zval
注意:数组和对象这类复合类型在生成zval时,会为每个单元生成一个zval
3、我们经常说每个变量都有一个内存地址,那这个zval和变量的内存地址,这俩有什么关系吗?
定义一个变量会开辟一块内存,这块内存好比一个盒子,盒子里放了zval,zval里保存了变量的相关信息,需要开辟多大的内存,是由zval所占空间大小决定的
zval是内存对象,垃圾回收的时候会把zval和内存地址(盒子)分别释放掉
4、引用计数、变量分离、写时拷贝
zval中的refcount和is_ref还没有介绍,我们知道PHP是一个长时间运行的服务器端脚本。那么对于它来说,效率和资源占用率是一个很重要的衡量标准,也就是说,PHP必须尽量减少内存占用率。考虑下面这段代码:
第一行代码创建了一个字符串变量,申请了一个大小为9字节的内存,保存了字符串“laruence”和一个NULL( )的结尾
第二行定义了一个新的字符串变量,并将变量var的值“复制”给这个新的变量
第三行unset了变量var
这样的代码是很常见的,如果PHP对于每一个变量赋值都重新分配内存,copy数据的话,那么上面的这段代码就要申请18个字节的内存空间,为了申请新的内存,还需要cpu执行某些计算,这当然会加重cpu的负载
而我们也很容易看出来,上面的代码其实根本没有必要申请两份空间,当第三句执行后,$var被释放了,我们刚才的设想(申请18个字节内存空间)突然变的很滑稽,这次复制显得好多余。如果早知道$var不用了,直接让$var_dup用$var的内存不就行了,还复制干嘛?如果你觉得9个字节没什么,那设想下如果$var是个10M的文件内容,或者20M,是不是我们的计算机资源消耗的有点冤枉呢?
呵呵,PHP的开发者也看出来了:
刚才说了,PHP中的变量是用一个存储在symbol_table中的符号名,对应一个zval来实现的,比如对于上面的第一行代码,会在symbol_table中存储一个值“var”,对应的有一个指针指向一个zval结构,变量值“laruence”保存在这个zval中,所以不难想象,对于上面的代码来说,我们完全可以让“var”和“var_dup”对应的指针都指向同一个zval就可以了(额,鸟哥一会说hash table,一会说symbol_table,暂且理解为symbol_table是hash table的子集)
PHP也是这样做的,这个时候就需要介绍一下zval结构中的refcount字段了
refcount,引用计数,记录了当前的zval被引用的次数(这里的引用并不是真正的 & ,而是有几个变量指向它)
比如对于代码:
第一行,创建了一个整形变量,变量值是1。 此时保存整形1的这个zval的refcount为1
第二行,创建了一个新的整形变量(通过赋值的方式),变量也指向刚才创建的zval,并将这个zval的refcount加1,此时这个zval的refcount为2
所以,这个时候(通过值传递的方式赋值给别的变量),并没有产生新的zval,两个变量指向同一zval,通过一个计数器来共用zval及内存地址,以达到节省内存空间的目的
当一个变量被第一次创建的时候,它对应的zval结构的refcount的值会被初始化为1,因为只有这一个变量在用它。但是当你把这个变量赋值给别的变量时,refcount属性便会加1变成2,因为现在有两个变量在用这个zval结构了
PHP提供了一个函数可以帮助我们了解这个过程debug_zval_dump
输出:
long(1) refcount(2)
long(1) refcount(3)
如果你奇怪 ,var的refcount应该是1啊?
我们知道,对于简单变量,PHP是以传值的形式传参数的。也就是说,当执行debug_zval_dump($var)的时候,$var会以传值的方式传递给debug_zval_dump,也就是会导致var的refcount加1,所以只要能看到,当变量赋值给一个变量以后,能导致zval的refcount加1这个结果即可
现在我们回头看上面的代码, 当执行了最后一行unset($var)以后,会发生什么呢?
unset($var)的时候,它删除符号表里的$var的信息,准备清理它对应的zval及内存空间,这时它发现$var对应的zval结构的refcount值是2,也就是说,还有另外一个变量在一起用着这个zval,所以unset只需把这个zval的refcount减去1就行了
上代码:
输出:
string(8) "laruence" refcount(2)
但是,对于下面的代码呢?
很明显在这段代码执行以后,$var_dup的值应该还是“laruence”,那么这又是怎么实现的呢?
这就是PHP的copy on write机制(简称COW):
PHP在修改一个变量以前,会首先查看这个变量的refcount,如果refcount大于1,PHP就会执行一个分离的过程(在Zend引擎中,分离是破坏一个引用对的过程)
对于上面的代码,当执行到第三行的时候,PHP发现$var想要改变,并且它指向的zval的refcount大于1,那么PHP就会复制一个新的zval出来,改变其值,将改变的变量指向新的zval(哪个变量指向新复制的zval其实已经无所谓了),并将原zval的refcount减1,并修改symbol_table里该变量的指针,使得$var和$var_dup分离(Separation)。这个机制就是所谓的copy on write(写时复制,这里的写包括普通变量的修改及数组对象里的增加、删除单元操作)
如果了解了is_ref之后,上面说的并不严谨
上代码测试:
输出:
long(1) refcount(2)
string(8) "laruence" refcount(2)
现在我们知道,当使用变量复制的时候 ,PHP内部并不是真正的复制,而是采用指向相同的zval结构来节约开销。那么,对于PHP中的引用,又是如何实现呢?
这段代码结束以后,$var也会被间接的修改为1,这个过程称作(change on write:写时改变)
那么ZE是怎么知道,这次的复制不需要Separation呢?
这个时候就要用到zval中的is_ref字段了:
对于上面的代码,当第二行执行以后,$var所代表的zval的refcount变为2,并且设置is_ref为1
到第三行的时候,PHP先检查var_ref对应的zval的is_ref字段(is_ref 表示该zval是否被&引用,仅表示真或假,就像开关的开与关一样,zval的初始化情况下为0,即非引用),如果为1,则不分离,直接更改(否则需要执行刚刚提到的zval分离),更改共享的zval实际上也间接更改了$var的值,因为引擎想所有的引用变量都看到这一改变
php源码做了这样一个判断,大体逻辑示意如下:
如果这个zval中的if_ref为1(即被引用),或者该zval引用计数小于2
任何一种方式:都不会进行分离
尽管已经存在写时复制和写时改变,但仍然还存在一些不能通过is_ref和refcount来解决的问题
对于如下的代码,又会怎样呢?
这里$var、$var_dup、$var_ref三个变量将共用一个zval结构(其实这是不可能的,一个zval不可能既被&,又被指向),有两个属于change-on-write组合($var和$var_ref),有两个属于copy-on-write组合($var和$var_dup),那is_ref和refcount该怎样工作,才能正确的处理好这段复杂的关系呢?
答案是不可能!在这种情况下,变量的值必须分离成两份完全独立的存在
当执行第二行代码的时候,和前面讲过的一样,$var_dup 和 $var 指向相同的zval, refcount为2
当执行第三行的时候,PHP发现要操作的zval的refcount大于1,则PHP会执行Separation(也就是说php将一个zval的is_ref从0设为1 之前,当然此时refcount还没有增加,会看该zval的refcount,如果refcount>1,则会分离), 将$var_dup分离出去,并将$var和$var_ref做change on write关联。也就是,refcount=2, is_ref=1;
所以内存会给变量var_dup 分配出一个新的zval,类型与值同 $var和$var_ref指向的zval一样,是新分配出来的,尽管他们拥有同样的值,但是必须通过两个zval来实现。试想一下,如果三者指向同一个zval的话,改边 $var_dup 的值,那么 $var和$var_ref 也会受到影响,这样就乱套了
图解:
下面的这段代码在内核中同样会产生歧义,所以需要强制复制!
也就是说一个zval不会既被引用,又被指向,必须分离
基于这样的分析,我们就可以让debug_zval_dump出refcount为1的结果来:
输出:
string(8) "laruence" refcount(1)
为什么结果是refcount(1)呢
debug_zval_dump()中参数是引用的话,refcount永远为1
小结:
这两段代码在执行的时候是这样的逻辑:
PHP先看变量指向的zval是否被引用,如果是引用,则不再产生新的zval
甭管哪个变量引用了它,比如有个变量$a被引用了,$b=&$a,就算自己引用自己$a=&$a,$a所指向的zval都不会被复制,改变其中一个变量的值,另一个值也被改变(写时改变)
如果is_ref为0且refcount大于1,改变其中一个变量时,复制新的zval(写时复制)
5、垃圾回收概述
refcount和is_ref这两个家伙与垃圾回收有关(garbage collection简称gc)
PHP的垃圾回收全靠这俩字段了。其中refcount表示当前有几个变量引用此zval,而is_ref表示当前zval是否被按引用引用
PHP5.2中的垃圾回收算法 —— Reference Counting
PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,任何关联到某个zval的变量离开它的作用域(比如:函数执行结束),或者把变量unset掉,refcount也会减1
当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval,计数器就是refcount
Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露(具体原因百度)
由于Reference Counting的这个缺陷,PHP5.3改进了垃圾回收算法
PHP5.3的垃圾回收算法仍然以引用计数为基础,但是不再是使用简单计数作为回收准则,而是使用了一种同步回收算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出
这里只需要了解垃圾回收是以引用计数为基础的就可以
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/30294.html
摘要:但是对于结构体中的和字段我们一直都没有详细介绍过,而这两个字段其实是和变量之间赋值的原理有着密切的关系的。 上周我们从底层的角度介绍了php变量从生成->常量赋值->销毁的完整生命周期(不了解的同学可以翻看一下前面的文章php底层原理之变量(一)),但是我们留了一个思考,不知道大家有答案了没,变量之间的赋值在底层又是如何实现的呢? 变量之间赋值 php变量的zval结构,我们已经介绍了...
摘要:执行原理是一门应用非常简单,开发效率极高的一门语言,其弱类型的变量能省去程序员大量的定义变量类型转换等的时间和精力。程序最终被翻译为一组处理函数的顺序执行。只有减为时才会真正执行销毁操作。 PHP执行原理 php是一门应用非常简单,开发效率极高的一门语言,其弱类型的变量能省去程序员大量的定义变量、类型转换等的时间和精力。它是一种适用于web开发的动态语言。 1. php设计的原理和特点...
摘要:本文主要是针对,的话可以移步到庆哥的博客看,还有就是小菜我读的是内核剖析这本书。接下来我会使用到来调试源码本文有参照博客中的部分内容以及代码。 前言 工作+实习快一年了,搞php后端开发,一直很迷茫怎么提高自己,就先从php源码开始吧,本人比较菜,本文章写的比较赶时间,所以有什么错误或者漏掉的地方,望各位大神指正,多交流才能成长嘛,嘿嘿。本文主要是针对php7,php5的话可以移步到庆...
摘要:插入一个元素时先将元素按先后顺序插入数组,位置是,再根据的哈希值映射到散列表中的某个位置,将存入这个位置查找时先在散列表中映射到,得到在数组的位置,再从数组中取出元素。目前只有两种类型会使用这种机制。 1.变量结构 typedef struct _zval_struct zval; typedef union _zend_value { zend_long ...
阅读 3756·2023-04-25 19:07
阅读 3506·2021-11-22 12:02
阅读 3098·2021-10-12 10:11
阅读 3877·2021-09-03 10:49
阅读 2869·2019-08-30 13:21
阅读 2971·2019-08-30 11:14
阅读 2062·2019-08-29 15:40
阅读 2852·2019-08-28 18:29