引用链接:php变量与gc
栈区stack
存储参数值,局部变量,维护函数调用关系
堆区heap
动态内存区域,随时申请和释放,需要自行处理生存周期
全局区(静态区)
存储全局和静态变量
字面量区
常量字符串存储区
程序代码区
存储二进制代码
PHP5的zval
zval核心是一个zvalue_value的union和zend_uchar类型type组成,5.3以后引入refcount__gc字段通过引用计数进行垃圾回收,同时新增了新的is_ref_gc来标记是否是引用类型
_zval_value只有5个字段,但是PHP有8种数据结构,布尔型、整型和资源型都是lval字段存储的,dval存浮点,str存字符串,ht存数组,obj存对象,如果所有字段都是0就是null
_zvalue_value的lval、dval为8字节,str12字节,obj为12字节,因为内存对齐,需要两个8字节来存,所以一共为16字节
_zval_struct的refcount__gc为4、type和is_ref_gc都是1,所以需要24字节
申请一个变量是(zval*)emalloc(sizeof(zval_gc_info))
zval_gc_info是一个zval结构体和一个u联合体组成,u为8字节
实际申请一个变量是32字节
因为整形和浮点型不需要进行gc,所以会有内存浪费
在开启zend内存池,zval_gc_info在内存池中分配,内存池会为每个zval_gc_info额外申请一个大小为16字节的zend_mm_block结构体
最终一个变量实际占用48字节
PHP5的问题
1.PHP5最大的问题就是zend_object_value需要12个字节,这个应该很容易优化掉,如把它移动出来用一个指针代替
2.zend_struct每一个字段都有明确的含义,没有预留任何自定义字段,导致做优化时候需要存储一些zval相关信息时候不得不采用其他结构体映射,或者外部包装打补丁方式来扩容zval如zval_gc_info,就是扩容了u的union,比如GC只关心IS_ARRAY和IS_OBJECT类型但是要用到32个字节
3.PHP的zval大部分都是按值传递, 写时拷贝的值, 但是有俩个例外, 就是对象和资源, 他们永远都是按引用传递, 这样就造成一个问题, 对象和资源在除了zval中的引用计数以外, 还需要一个全局的引用计数, 这样才能保证内存可以回收. 所以在PHP5的时代, 以对象为例, 它有俩套引用计数, 一个是zval中的, 另外一个是obj自身的计数
获取object为EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj,要经过多次内存读取
4.PHP中大量的计算都是面向字符串的, 然而因为引用计数是作用在zval的, 那么就会导致如果要拷贝一个字符串类型的zval, 我们别无他法只能复制这个字符串. 当我们把一个zval的字符串作为key添加到一个数组里的时候, 我们别无他法只能复制这个字符串;PHP中大量的结构体都是基于Hashtable实现的, 增删改查Hashtable的操作占据了大量的CPU时间, 而字符串要查找首先要求它的Hash值, 理论上我们完全可以把一个字符串的Hash值计算好以后, 就存下来, 避免再次计算等等
php7的zval
在_zval_struct中,还有两个重要字段u1、u2
v和type_info公用一块内存,长度均是4,修改type_info等同于修改v中的值,type_info就是v中的4个char的组合
type | 记录变量的类型 PHP7中的字段类型使用u1.v.type来表示 |
type_flags | 变量类型特有的标记,不同类型的变量对应的flag也不同 常量类型、不可变类型、需要引用计数的类型、可能包含循环引用的类型、可被复制的类型 |
const_flags | 常量类型的标记 |
reserved | 保留字段 如字符串,u1.v.type值为6(IS_STRING),字符串又是可以引用和可以拷贝的,所以u1.v.type_flag值为24(S_TYPE_COPYABLE|IS_TYPE_REFCOUNTED),这样u1.type_info为6150 |
IS_UNDEF为标记为未定义,表示数据可以被覆盖或者删除,如Unset操作,PHP7不会直接将数据从分配给hashtable的内存中删除,而是先将该元素所在的bucket位置标记为IS_UNDEF,当hashtable中的IS_UNDEF元素个数达到一定阈值时候,进行rehash操作时候再将元素覆盖或者删除
整型和浮点型的值拷贝
$a = 10
$a = zval_1(u1.v.type=IS_LONG,value.lval=10)
$b = $a
$a = zval_1(u1.v.type=IS_LONG,value.lval=10)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
因为zval只有16字节,所以没有直接做写时拷贝,而是直接做了拷贝
$a = 20
$a = zval_1(u1.v.type=IS_LONG,value.lval=20)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
unset($a)
$a = zval_1(u1.v.type=IS_UNDEF,value.lval=20)
$b = zval_2(u1.v.type=IS_LONG,value.lval=10)
字符串
冗余了hash值h,避免了在数组操作中hash值的重复计算
len表示字符串的长度
val记录了字符串的内容
字符串通过zval.str指向zend_string结构体
引用
PHP5在采用了引用计数后,使用refcount__gc来记录次数,同时使用is_ref_gc来记录是否是引用类型
$a = 'hello'
$a -> zval1(type=IS_STRING,refcount_gc=1,is_ref_gc=0)
$b = $a
$b,$a -> zval1(type=IS_STRING,refcount_gc=2,is_ref_gc=0)
$c = &$b
$a -> zval1(type=IS_STRING,refcount_gc=1,is_ref_gc=0)
$c,$b -> zval2(type=IS_STRING,refcount_gc=2,is_ref_gc=1)
PHP7的zval没有存储引用计数相关的信息,引入了新的类型IS_REFERENCE来处理&
$a = 'hello'.time()
$a -> zend_string(refcount=1,val)
$b = $a
$b,$a -> zend_string(refcount=2,val)
$c = &$b
$a -> zend_string(refcount=2,val)
$b,$c ->zval(type=IS_REFRENCE,refcount=2)->zend_string(refcount=2,val)
当使用&操作符时候,会创建一种新的中继结构体zend_reference,这个结构体会指向真正的zend_string结构体
PHP5的情况如下
PHP7的情况如下
对于zval在value字段中就能保存下来的,就不会对他们进行引用计数
$a = 123
$b = $a //此时refcount和is_ref都是0
$c = &$a; //此时a和c的refcount是2,is_ref是1,b的refcount和is_ref都是0
因为&操作会申请一个新的zend_reference结构,将zend_reference指向原来的zval_struct.value
不可变数组,伪引用计数为2
PHP7的改进优化
1.zval只需要16字节,保留了扩充字段u1、u2两个union,u2为辅助字段,如u2的next用来取代hashtable中原来的拉链法指针
2.PHP5时候的IS_BOOL拆分成了IS_FALSE和IS_TRUE,类型检查的时候更快
3.IS_LONG和IS_DOUBLE在拷贝的时候直接赋值,省去了大量的引用计数操作,如$a = 1,refcount为0;对于只有类型而没有值的也不需要引用计数了IS_NULL、IS_FALSE、IS_TRUE
4.对于复杂类型,一个value保存不下来的,就用value来保存一个指针, 这个指针指向这个具体的值, 引用计数也随之作用于这个值上, 而不在是作用于zval上了
以IS_ARRY为例子
zval.value.arr将指向上面的这样的一个结构体, 由它实际保存一个数组, 引用计数部分保存在zend_refcounted_h结构中
所有的复杂类型的定义, 开始的时候都是zend_refcounted_h结构, 这个结构里除了引用计数以外, 还有GC相关的结构. 从而在做GC回收的时候, GC不需要关心具体类型是什么, 所有的它都可以当做zend_refcounted*结构来处理
对象
PHP5的对象存储在_zend_object_value结构体中
handle是一个无符号的int,通过handler可以在全局的对象池里面索引到指定对象;handlers指向了一个包含多个函数指针的结构体,但是对象的真正数据并没有在这里,而是存在全局的EG(objects_store)中
对象在zend_object_store_bucket中维护了另外一个refcount来记录对象的引用计数
PHP5对象的问题是两套引用计数和获取对象的多次内存读取,使得对象效率比较低
PHP7中对象
PHP7对象的属性存储在properties_table数组中,而properties是一个hashtable,key为对象属性的名字,value为属性值在properties_table数组中的偏移量,通过偏移量去查找真正的值
PHP5和7的change on write
$a = "123".time()
a;
a;
在PHP5中,a是引用关系,refcount=2,is_ref_gc为1,当a时候,发现a的复制
在PHP7中,a首先生成一个refernce类型,然后因为此时有俩个变量引用它所以zend_reference这个结构的引用计数zval.value.ref->gc.refcount为2,然后a时候,让$c指向zval.value.ref->val就可以了,没有产生复制
gc基本
PHP7中复杂类型的引用计数都维护在各个结构体头部的gc中
gc是一种内存管理机制,但一个变量不需要时候应该被释放,一种方式是使用引用计数,通过对数据存储的物理空间多附加一个计数器,当其他数据与其相关就自增,定期检查存储对象的计数器
PHP7的gc实现方式是定期遍历和标记若干存储对象的数组,再通过算法将是垃圾的物理空间回收
总大小为8字节
zend_uchar type冗余了一份u1.v.type,1字节
zend_uchar flags可以是字符串类型或者是数组类型等,1字节
gc_info 为2字节,标记当前元素的颜色和位置,黑色、白色、灰色、紫色
如
$i = 0;
$a = "hello" . $i;
$b = $a;
a并没有进行内存拷贝,而是直接指向了同一个zend_string结构体
unset($a)
refcount为1
unset($b)
refcount为0,调用ZVAL_UNDEF(var),将type标记为IS_UNDEF
循环引用问题
PHP7中使用&会改变等号两边zval的类型为IS_REFERENCE,引用计数会在新的结构体zend_reference中,并且引用计数为2
如果等号两边是同一个变量,那么就自己引用自己
$a = []
$a[] = &$a;

当执行unset($a)操作,$a所在的zval类型被标记为IS_UNDEF,zend_reference结构体的引用计数减1,但是仍然大于0,后面的结构体可能变成垃圾,如果循环引用不处理可能会造成内存泄漏,gc会将这部分可能是垃圾的数据收集到缓冲区,同时加入root环
$a = []
$a[] = &$a;
unset($a)
a的第一个元素指向a,a的zval的refcount为2,unset($a), refcount为1,但是不会被回收

PHP是通过符号表(Symbol Table)存储变量符号的,全局有一个符号表,而每个复杂类型如数组或对象有自己的符号表,因此上面代码中,a和a[0]是两个符号,但是a储存在全局符号表中,而a[0]储存在数组本身的符号表中,且这里a和a[0]引用同一个zval(当然符号a后来被销毁了)
##### gc
PHP7的gc为垃圾收集器将可能是垃圾的元素收集在回收池中,然后由垃圾回收算法回收
1.如果一个变量value的refcount减少到0,此value可以被释放,不属于垃圾(gc不会处理)
2.如果一个变量value的refcount减少后大于0,此value不可以被释放,属于垃圾
目前垃圾是会出现在array和object中,object是成员属性引用对象本身
unset($a)后,对a进行析构函数将refcount-1,若为0则说明可以直接释放内存,若大于0则放到gc_root_buffer中,每个zval只可放一次,依据是zval所在的zval_gc_info中gc_root_buffer的颜色是否为紫色
在自动GC中,在zval断开value的指向时如果发现refcount=0会直接释放,发生断开的常见的为修改变量与函数返回,函数返回会释放所有的局部变量,把所有的局部变量的refcount-1
如果zval的refcount减少到0,那么zval可以被释放,不属于垃圾
如果zval的refcount减少后大于0,那么zval还不能被释放,zval可能是一个垃圾,放入缓存区
如下代码会造成内存溢出,因为关闭了gc导致res不能被回收


gc_root_buffer是一个双向链表,同时记录引用计数相关信息,zend_gc_globals维护着gc的整个信息
| | |
| ------ | ------ |
|gc_enabled | 是否开启gc|
|gc_active | 垃圾回收算法是否运行|
|gc_full | 垃圾缓冲区是否满了|
|buf | 垃圾缓冲区,默认大小为10000个节点,第0个节点保留,不会使用(#define GC_ROOT_BUFFER_MAX_ENTRIES 10001)
|roots | 指向缓冲区中最新加入的可能是垃圾的元素|
|unused | 指向缓冲区中没有使用的位置,在没有启动gc算法前,指向空|
|first_unused | 指向缓冲区中第一个未使用的位置,新的元素插入缓冲区后,指针向后移动一位|
|last_unused | 指向缓冲区最后一个位置|
| to_free | 待释放的列表|
| next_to_free | 下一个待释放的列表|
| gc_runs | 记录gc算法运行的次数,当缓冲区满了,才会运行gc|
| collected | 记录gc算法回收的垃圾数|
zend_gc_globals大小为120字节,PHP7维护了一个全局变量zend_gc_globals的hashtable

存取值的宏为GC_G(v)
gc的初始化


###### 垃圾回收过程
1.类型是数组和对象
2.没有在缓存区存在
3.没有被标记过
4.将其gc_info标记为紫色,且记录其在缓冲区的位置
当缓冲区满了,在收集到新的元素就会触发gc算法,引用计数大于0说明还有其他地方使用,那么先将引用计数-1,如果为0则说明是垃圾,需要被回收。反之说明不是垃圾,需要将其从回收池移除
###### gc算法过程
1.对roots环中的每个元素进行深度优先遍历,将每个元素中的gc_info为紫色的标记为灰色,且引用计数-1
2.扫描roots环中gc_info为灰色的元素,如果发现引用计数仍然大于0,说明不为垃圾,那么将其颜色重新标记为黑色,并且引用计数+1,;如果发现引用计数为0,标记为白色
3.扫描roots环,将gc_info为黑色的从roots中移除,然后的白色的元素进行深度优先遍历,将其引用计数+1,然后将roots链表移动到待释放的列表to_free中
4.释放to_free列表的元素

对象是类的实例,有继承类的默认属性表default_properties_table,同时类支持动态属性,所以也有自己的properties_table,在对类进行深度优先遍历时候会将两个表重建合并
###### gc配置
* php.ini

* gc_enable()
* gc_disable()
* gc_collect_cycles() //强制进行垃圾回收
关闭了gc时候,还会记录到root根缓冲区,但是当根缓冲区满了不会自动进行垃圾回收