什么是引用计数在PHP的数据结构中,引用计数指的是每一个变量。除了保存它们的类型和值外,它还保存了两个额外的内容,一个是当前变量是否被引用,另一个是被引用的次数。为什么要多存这两个内容呢?当然是为了垃圾回收(GC)。也就是说,当引用数为0时,该变量不再被使用,可以通过GC回收,释放占用的内存资源。任何程序都不可能无限期地占用内存资源。过多的内存使用往往会带来一个严重的问题,那就是内存泄漏,而GC是PHP底层自动为我们完成内存销毁的。同样必须手动释放。如何查看引用计数?我们需要安装xdebug扩展,然后使用xdebug_debug_zval()函数查看指定内存的详细信息,例如:$a="IamaString";xdebug_debug_zval('a');//a:(refcount=1,is_ref=0)='IamaString'从上面的内容可以看出$a变量的内容是一个字符串比如IamaString。括号中的refcount为引用次数,is_ref表示变量是否被引用。让我们看看这两个参数是如何通过变量赋值来改变的。$b=$a;xdebug_debug_zval('a');//a:(refcount=1,is_ref=0)='IamaString'$b=&$a;xdebug_debug_zval('a');//a:(refcount=2,is_ref=1)='IamaString'当我们进行正常赋值时,refcount和is_ref没有变化,但是当我们进行引用赋值时,可以看到refcount变成了2,is_ref变成了1。这意味着当前的\$a变量是通过引用赋值的,它的内存符号表是为$a和$b这两个变量服务的。$c=&$a;xdebug_debug_zval('a');//a:(refcount=3,is_ref=1)='IamaString'unset($c,$b);xdebug_debug_zval('a');//a:(refcount=1,is_ref=1)='IamaString'$b=&$a;$c=&$a;$b="IamaStringnew";xdebug_debug_zval('a');//a:(refcount=3,is_ref=1)='IamaStringnew'unset($a);xdebug_debug_zval('a');//a:nosuchsymbol继续添加$c引用赋值,可以看到refcount会不断增加。然后在unset$b和$c之后,refcount归为1,但是这个时候需要注意的是is_ref还是1,也就是这个变量被引用了,thisis_ref会变成1,即使被引用的变量已经有了未设置,值保持不变。最后我们unset$a,显示没有这个符号。当前变量已被销毁,不再是可用的符号引用。(注意PHP中的变量对应的是内存的符号表,不是真正的内存地址)对象的引用计数和普通类型的变量一样,对象变量也使用同样的计数规则。//对象引用计数classA{}$objA=newA();xdebug_debug_zval('objA');//objA:(refcount=1,is_ref=0)=classA{}$objB=$objA;xdebug_debug_zval('objA');//objA:(refcount=2,is_ref=0)=classA{}$objC=$objA;xdebug_debug_zval('objA');//objA:(refcount=3,is_ref=0)=classA{}unset($objB);classC{}$objC=newC;xdebug_debug_zval('objA');//objA:(refcount=1,is_ref=0)=classA{}但是这里需要付费注意是的,对象的符号表是一个已建立的连接,也就是说重新实例化或者修改$objC为NULL不会影响$objA的内容。这方面的知识在前面的对象赋值中在PHP中。是报价吗?文章中已经解释过了。对象的正常赋值操作也是引用类型的符号表赋值,所以我们不需要加&符号。数组引用计数//数组引用计数$arrA=['a'=>1,'b'=>2,];xdebug_debug_zval('arrA');//arrA:(refcount=2,is_ref=0)=array(//'a'=>(refcount=0,is_ref=0)=1,//'b'=>(refcount=0,is_ref=0)=2//)$arrB=$arrA;$arrC=$arrA;xdebug_debug_zval('arrA');//arrA:(refcount=4,is_ref=0)=array(//'a'=>(refcount=0,is_ref=0)=1,//'b'=>(refcount=0,is_ref=0)=2//)unset($arrB);$arrC=['c'=>3];xdebug_debug_zval('arrA');//arrA:(refcount=2,is_ref=0)=array(//'a'=>(refcount=0,is_ref=0)=1,//'b'=>(refcount=0,is_ref=0)=2//)//添加现有元素$arrA['c']=&$arrA['a'];xdebug_debug_zval('arrA');//arrA:(refcount=1,is_ref=0)=array(//'a'=>(refcount=2,is_ref=1)=1,//'b'=>(refcount=0,is_ref=0)=2,//'c'=>(refcount=2,is_ref=1)=1//)在调试数组的时候,我们会发现两个有趣的事情。一是数组中的每个元素都有自己独立的引用计数。这也比较容易理解。每个数组元素都可以看作是一个单独的变量,但数组是这些变量的哈希集合。如果对象中有成员变量,同样的效果适用。当数组中的一个元素通过&引用赋值给另一个变量时,这个元素的refcount会增加,而不影响整个数组的refcount。第二,数组默认的refcount为2,其实这是PHP7之后的新特性。当数组被定义和初始化时,数组将被转换成一个不可变数组(immutablearray)。为了区别于普通数组,这个数组的refcount是从2开始的。当我们修改这个数组中的任意一个元素时,这个数组会变回普通数组,即refcount会变回1。大家可以试试这个你自己官方解释为什么这样做是为了效率。具体原理可能还需要深挖PHP7的源码才知道。内存泄漏需要注意的事情其实PHP底层已经帮我们实现了GC机制,所以我们不需要太在意变量的销毁和释放。但是,我们必须要注意,对象或数组中的元素是可以给自己赋值的,也就是说给元素本身赋值就变成了循环引用。那么这个对象基本上是不太可能被GC自动销毁的。//对象循环引用类D{public$d;}$d=newD;$d->d=$d;xdebug_debug_zval('d');//d:(refcount=2,is_ref=0)=classD{//public$d=(refcount=2,is_ref=0)=...//}//数组循环引用$arrA['arrA']=&$arrA;xdebug_debug_zval('arrA');//arrA:(refcount=2,is_ref=1)=array(//'a'=>(refcount=0,is_ref=0)=1,//'b'=>(refcount=0,is_ref=0)=2,//'arrA'=>(refcount=2,is_ref=1)=...//)不管是对象还是数组,打印调试的时候如果有...这样的省略号,那么你的程序会存在循环引用。在上一篇关于PHP中对象复制的文章中,我们也谈到了这个循环引用问题,所以这个问题应该是我们在日常开发中应该时刻注意的。总结引用计数是了解垃圾回收机制的前提,也正是因为现代语言中有类似的垃圾回收机制,我们的编程才变得更加简单和安全。那么有人说这些根本不是日常开发用的?仅仅因为你不使用它并不意味着你不应该学习它。就像循环引用的问题一样,当代码中塞满了很多相似的代码时,系统崩溃只是时间问题。因此,这些知识是我们推进到更高级程序所需要的。必不可少的内容。测试代码:https://github.com/zhangyue0503/dev-blog/blob/master/php/202004/source/PHP%E7%9A%84%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E6%98%AF%E4%BB%80%E4%B9%88%E6%84%8F%E6%80%9D%EF%BC%9F.php参考文档:https://www.php.net/manual/zh/features.gc.refcounting-basics.phphttps://ask.csdn.net/questions/706390https://www.jianshu.com/p/52450a61354d各媒体平台可以搜索【硬核项目经理】
