浅谈PHP的GC机制
对于C/C++这种需要自己手动进行内存的分配与释放,自己搞变量的内存管理的确牛叉,对程序员的确是一个很大的考验。
常见的实现GC的方式:在函数中定义变量时分配一块内存,用于保存zval以及对应的value结构,在函数返回时再将内存释放,如果在函数执行期间,该变量作为参数调用了其他函数或者赋值给了其他变量,则把变量赋值一份,变量之间相互独立,则不会出现冲突。
这么处理虽然简单但有一个大问题就是深拷贝带来的效率问题,而且内存严重浪费。为了解决这个问题比较通用的方案是:引用计数+写时复制。PHP变量的内存管理正是基于这两点实现的。当变量赋值、传递时不是直接进行深拷贝,而是多个变量共用一个value,引用计数来记录value被多少个变量在使用,当某个变量的value发生改变无法继续与其他变量共用value,这个时候就需要深拷贝分离value,这就是写时复制。
引用计数
引用计数用来记录当前有多少个zval指向同一个zend_value。当有新的zval指向这个value时,计数器加1;当zval销毁时,计数器减1。当引用计数为0时,表示此value已经没有被任何变量指向,可以释放了。
PHP7中将变量的引用计数保存在zend_value中,之前版本是保存在zval结构中。但并不是所有类型都用到引用计数,没有具体value结构的类型不会用到,如整型、浮点型、布尔型、NULL,它们的值直接通过zval保存,因此这些类型不会共用value而是深拷贝。还有两种特殊情况也不会用到:内部字符串、不可变数组。
1 | $a = "hi"; |
这里refcount = 0,没用到引用计数,因为这属于内部字符串,这里的字符串内容是唯一不变的,其生命周期为整个请求执行期,request完成后会统一释放,这样就无需在运行期间通过引用计数来管理内存。
1 | $a = "hi".time(); |
这里refcount = 2
写时复制
这么多变量使用了引用计数必然会存在搞事的,当出现有一个变量修改value情况,这个时候就要对value进行分离了,发生修改的变量会复制一份数据出来进行修改,同时断开与原来value的指向,指向新的value。
1 | $a = array(1,2,3); |
这里$a的gc.refcount=2->1,$c的gc.refcount=1;
当然也不是所有类型的value都可以进行写时复制,比如对象和资源就无法进行复制。如果多个变量指向同一个对象,当其中一个变量修改对象时。其修改将反映到所有变量上。事实上只有array、string这两种支持value的分离。
回收机制
回收时机
在自动GC机制中,在zval断开value的指向时如果发现refcount=0则会直接释放value。断开情况分为修改变量和函数返回。函数返回时会释放所有局部变量,也就是把所有局部变量的引用计数减1。除了自动GC,PHP也可以通过unset()函数主动销毁一个变量。
垃圾回收
如果变量存在循环引用,就会导致无法回收,造成内存泄漏。比如数组中的元素指向了数组,这样一来就存在自家孩子坑爹的情况,即使断开外部所有连接,refcount仍然大于0。
1 | $a = array(1,2,3); |
unset($a)之前,变量a的类型为引用,该引用的refcount=2,一个来自$a,另一个来自$a[1];unset($a)之后减少了一次引用,此时没有任何外部引用了,但是数组中仍然有一个元素在默默指向该引用。这种因为循环引用而导致的无法释放的变量称为垃圾。PHP引入了垃圾回收器来进行垃圾回收。
目前垃圾只会出现在array和object这两种类型中。垃圾回收器把收集到的可能垃圾保存在buffer缓冲区中,收集的时机是refcount减少时。
对象的情况则是成员属性引用对象自身导致的。
回收算法
当垃圾回收器收集到的可能垃圾达到一定数量后,就会启动垃圾鉴定、回收程序。回收算法的原理大概就是:既然垃圾是由于成员变量引用自身导致的,那么就对value的所有成员减一遍引用计数,如果发现value本身refcount变为0,则就表明其引用全部来自自身成员。具体算法和结构现在不写了,也不懂,以后懂了再补上。
原文作者: ybphp
原文链接: https://www.ybphp.com/2020/03/05/浅谈PHP的GC机制/
版权声明: 转载请注明出处(必须保留原文作者署名原文链接)