1 简介
C/C++语言中,如果想在堆
上分配变量,需要手动进行内存的分配与释放,变量的内存管理是见非常繁琐的事,稍有不慎就可能导致不可域值的错误。 PHP实现了自动GC机制,由语言自行管理。PHP中的变量是不需要手动释放的,内核帮我们实现了变量的内存管理,包括内存的分配与回收。
自动GC最简单的实现方式:在函数中定义变量时分配一块内存,用于保存zval及对应的value结构,在函数返回时再将内存释放,若函数执行期间改变来凝固作为参数调用了其他函数或复制给了其他变量,则把变量复制一份,变量之间相互独立,不会出现冲突。
(问题:深拷贝可能造成内存浪费,比如定义一个变量赋值给另一个变量,后面是只读操作的情况)
解决方式(PHP采用此种方式): 引用计数
+写时复制
。
- 当变量赋值、传递时多个变量公用一个value,引用计数用来记录value有多少个变量在使用;
- 当变量的value发生改变时,进行深拷贝。
注:long、double类型是使用的硬拷贝。
2 引用计数
引用计数用来记录当前有多少zval
指向同一个zend_value
.
引用计数:指在value中增加一个字段refcount记录指向当前value的数量,变量复制、函数传参时并不直接硬拷贝一份value数据,而是将refcount++,变量销毁时将refcount–,等到refcount减为0时表示已经没有变量引用这个value,将它销毁即可。
PHP7中将变量的引用计数保存在zend_value中(与之前版本不同)
之前变量中不同类型的结构体中都有一个相同的成员:gc,该结构用于保存引用计数的,其定义如下:
1 | // zend_type.h |
例1:
1 | $a = "time:" . time(); //$a -> zend_string_1(refcount=1) |
例2:
1 | $a = array(); // $a -> zend_array(refcount=1) |
问: 是所有变量类型会用到引用计数吗?如果不是,那么那些情况不会用到呢?
不是, 其中不会用到引用计数的情况如下:
- ①、没有具体value结构的类型是不会用到的,比如:整型、浮点型、布尔型、NULL,他们的值直接通过zval保存,赋值时采用深拷贝。
- ②、interned string:内部字符串,在PHP中写的函数名、类名、变量名、静态字符串等都是这种类型,定义:$a = “hi
“;后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:char *a = “hi“;,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。
(注:内部字符串与普通字符串的类型都是IS_STRING,它们并不是通过type进行区分的,而是通过zend_refcount_h.u.v.flag区分,内部字符串的flag值将包含IS_STR_INTERNED) - ③、immutable array:不可变数组,只有在用opcache的时候才会用到这种类型,不清楚具体实现,暂时忽略。
除了以上几种情况, 其余类型将会用到引用计数。
问: PHP内核是怎样区分value是否支持引用计数的呢?
使用zval.u1
中的类型掩码type_flag
字段, 这个字段除了标识value是否支持引用计数外还有其它几个标识位,按位分割
注:type_flag与zval.value->gc.u.flag不是一个值。
支持引用计数的value类型是zval.u1.type_flag & IS_TYPE_REFCOUNTED
。
1 | // IS_TYPE_REFCOUNTED = 4 |
下列类型会使用引用计数机制:
1 | | type | refcounted | |
3 写时复制
写时复制机制,它只有在必要的时候(即发生写的时候)才会进行深拷贝,可以很好地提升效率。
具体过程:变量使用了引用计数,当出现其中一个变量修改value的情况,这个时候就需要对value进行分离,发生改变的变量恢复至一份数据出来进行修改,同时断开原来value的指向,指向新的value。
例1:
1 | $a = array(1,2); |
例2:
1 | $a = array(1,2); |
注:不是所有类型都可以copy的,比如对象、资源就无法进行复制,也就是无法进行分离,如果多个变量指向同一个对象,当其中一个变量修改对象时,其修改将反映到所有变量上。事实上只有string、array两种支持value的分离,与引用计数相同,也是通过zval.u1.type_flag
标识value是否可复制的。
1 | // IS_TYPE_COPYABLE = 16 |
4 变量回收
PHP变量回收方式:
- 主动销毁:指的就是 unset。
- 自动销毁:指PHP的自动管理机制(GC机制).
- 在return时减掉局部变量的refcount,即使没有显式的return,PHP也会自动给加上这个操作;
- 写时复制时也会断开原来value的指向,这时候也会检查断开后旧value的refcount。
变量回收时机: zval断开value的指向时,若发现refcount=0则会直接释放value。其中发生断开的情况包括:
- 修改变量:修改变量时会断开原有value的指向
- 函数返回:函数返回时会释放所有的局部变量,也就是把所有局部变量的引用计数减1.
5 垃圾回收
PHP用过引用计数实现了变量的自动GC机制,还是会有一种情况GC机制无法解决,从而导致变量无法回首导致内存始终得不到释放,造成内存泄露。
造成内存泄漏的情况:循环引用
问: 什么是循环引用?
循环引用就是变量的内部成员引用了变量自身,比如数组中某个元素指向了数组,这样一来数组的引用计数中就有一个来自自身成员,当所有外部引用断开时,数组的refcount仍然大于0而得不到释放,但实际上这种变量不可能再被使用了。
垃圾收集器收集的时机:refcount减少时,即每次refcount减少都会试图收集
示例:
1 | $a = array(1); |
unset($a)前,变量a的类型为引用,该引用的refcount=2,一个来自于$a,一个来自于$a[1];示意图如下:
unset($a)后,减少了一次该引用的refcount,此时已经没有任何外部引用了,但是数组中仍然有一个元素指向该引用,如下图所示:
这种因为循环引用而导致无法释放的变量称之为垃圾,PHP引入了另一种机制来对这些垃圾进行回收,即垃圾回收器。
- 若一个变量value的refcount减少到0,那么此vlaue可以被释放掉,不属于垃圾。
- 若一个变量value的refcount减少之后大于0,那么此vlaue还不能被释放,此value可能成为一个垃圾。
垃圾回收器会将可能成为垃圾的value收集起来,等到达一定数量后开始启动垃圾鉴定程序,将真正的垃圾释放掉。
当前垃圾只会出现在array、object两种类型中
- array:数组中的某个成员指向了数组
- object:对象的成员属性引用对象本身
问:垃圾回收器怎样判断当前类型是否可回收?
垃圾回收器不是通过变量类型进行判断的,而是通过zval.u1.type_flag
标识进行判断,只有包含IS_TYPE_COLLECTABLE
标识的变量类型才会被收集。
1 | // IS_TYPE_COLLECTABLE = 8 |
5.1 回收算法
垃圾回收器把收集到的可能垃圾保存到一个buffer缓冲区中,当到达一定数量后就会启动垃圾鉴定、回收程序。
回收算法的原理:既然垃圾是由于成员引用自身导致的,那么就对value的所有成员减一遍引用计数,如果发现value本身refcount变为了0,则表明引用全部来自自身成员。
5.2 具体实现
垃圾回收器主要通过zend_gc_globals
这个这结构对垃圾进行管理,收集到的可能成为垃圾的vlaue就保存在这个结构的buf中,即垃圾缓冲区
。
1 | // zend_gc.h |
- buf用于保存收集到的value,他是一个数组,在垃圾回收器初始化时一次性分配了10001个gc_root_buffer,其中第一个buffer被保留,插入vlaue时直接取出可用结点即可。
- roots指向buf中最新加入的一个结点,root是一个双向链表的头部,之所以是一个双向链表,是因为bug数组中保存的只是有可能成为垃圾的vlaue吗,其中有些value在加入之后又被删除了,这样bug数组中就会出现一些空隙。
- first_unused一开始指向bug的第一个位置,有些元素插入roots时如果first_unused还没有到达buf尾部,则返回first_unused给最新的元素,然后执行first_unused++,直到last_unused.
下图为已经加入了2个gc的结构:
- unused成员,他的含义与first_unused类似,用来管理buf中开始加入后面又删除的结点,是一个单链表结构。也就是说first_unused是一直往后偏移的,直到buf的结尾,buf中间由于value删除而重新空闲的结点则由unused串起来。下次有新的value插入roots时优先使用unused的这些节点,其次才是first_unused的结点。
下图为移除了buf[1]的结构:
- 第一步,初始化 gc_init()初始化垃圾回收器:分配buf数组内存、设置first_unused/unused等
1 | ZEND_API void gc_init(void) |
- 第二步,垃圾回收,在Zend执行过程中如果销毁一个变量就会判断是否需要加入垃圾收集器。销毁一个zval会调用i_zval_ptr_dtor()进行处理。
1 | // file:zend_variables.h |
收集时首先会从buf中选择一个空闲节点,然后将vlaue的gc保存到这个结点中,若没有空闲节点则表明回收器已经满了,这个时候就会触发垃圾鉴定、回收程序。
1 | // file:zend_gc.c |
- 第三步,删除 删除的操作通过GC_REMOVE_FROM_BUFFER()完成。
1 | // file:zend_gc.h |
删除时,先根据gc_info取到gc_root_buffer,然后再从buf中移除,删除后再把空的gc_root_buffer插入到unused单链表尾部。
1 | // file:zend_gc.c |
当buf缓存区满了执行垃圾回收的过程如下:
1 | ZEND_API int zend_gc_collect_cycles(void) |