PHP垃圾回收机制

PHP垃圾回收机制是基于PHP的变量引用计数机制实现的,在了解引用计数机制之前,我们先了解一下PHP变量存储结构。

变量存储结构

在之前的文章PHP写时拷贝技术中,提到过PHP的变量容器通过refcont进行引用计数。在PHP7之后,zval结构发生了变化,以下面代码运行结果为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

$startMemory = memory_get_usage();

$i = 4;
xdebug_debug_zval('i');
echo 'memory: ', memory_get_usage() - $startMemory, " bytes\n";

$j = $i;
echo 'share memory: ', memory_get_usage() - $startMemory, " bytes\n";
xdebug_debug_zval('i');

$j = 10;

echo 'no share memory: ', memory_get_usage() - $startMemory, " bytes\n";
xdebug_debug_zval('i');
?>

# 运行结果
i: (refcount=0, is_ref=0)=4
memory: 32 bytes
share memory: 32 bytes
i: (refcount=0, is_ref=0)=4
no share memory: 32 bytes
i: (refcount=0, is_ref=0)=4

在运行$j=$i之后,变量$irefcount并没有发生变化,而且是0(在PHP7之前版本测试中,应该是2), 这主要是由于PHP7的相关的数据结构发生了变化。

在PHP7中,变量分为变量名变量值两部分,分别对应zvalzend_value

zval

1
2
3
4
5
6
7
8
9
10
// php 变量对应的c结构体
struct _zval_struct {
zend_value value;
union {
……
} u1;
union {
……
} u2;
};

zend_value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef union _zend_value {
zend_long lval;//整形
double dval;//浮点型
zend_refcounted *counted;//获取不同类型的gc头部
zend_string *str;//string字符串
zend_array *arr;//数组
zend_object *obj;//对象
zend_resource *res;//资源
zend_reference *ref;//是否是引用类型

// 忽略下面的结构,与我们讨论无关
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
ZEND_ENDIAN_LOHI(
uint32_t w1,
uint32_t w2)
} ww;
} zend_value;

zval的 value中就记录了引用计数zend_refcounted *counted这个类型,我们的垃圾回收机制也是基于此的。

zend_refcounted

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;

新的结构中,引用 (REFERENCE)变为了一种数据结构而不再只是一个标记位了。

引用计数原理

了解了php变量的内部存储结构之后,再了解下php变量赋值相关的原理和早期垃圾回收机制。

PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。

内存泄漏

但是php5.3版本之前的垃圾回收机制存在一个漏洞,即当数组或对象内部子元素引用其父元素,而此时如果发生了删除其父元素的情况,此变量容器并不会被删除,因为其子元素还在指向该变量容器,但是由于所有作用域内都没有指向该变量容器的符号,所以无法被清除,因此会发生内存泄漏,直到该脚本执行结束

如果你已经安装了Xdebug,你能通过调用函数 xdebug_debug_zval()显示”refcount”和”is_ref”的值。

举例:

1
2
3
$a = array( 'one' );
$a[] = &$a;
xdebug_debug_zval( 'a' );

由于该示例不好输出结果,用图表示,如图:

php底层原理之垃圾回收机制

举例:

1
2
unset($a);
xdebug_debug_zval('a');

如图:

php底层原理之垃圾回收机制

根缓冲机制

php5.3版本之后引入根缓冲机制,即php启动时默认设置指定zval数量的根缓冲区(默认是10000),当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量(默认是10000)后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题

为什么内存没有全部收回来

因为php的核心结构Hashtable,在定义的时候不可能一次性分配足够多的内存块,所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以当存入100个变量的时候符号表不够用了就进行一次扩容,当unset()时只是放了为变量值分配的内存,但是为变量名分配的内存还是在符号表中的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。

php并不是只要内存不够就去向OS申请内存,而是先申请一大块内存,然后将其中一部分分给申请者,这样再有逻辑需要申请内存的时候,就不需要再向OS申请内存了,避免了重复申请,只有当一大块内存不够用的时候再去申请。而当释放内存时,php并非把内存还给了OS,而是把内存轨道自己维护的空闲内存列表,以便重复利用。

垃圾回收相关的配置

  • zend.enable_gc,默认值为on,如果想关闭垃圾回收机制,可以设置为off

小知识点

  • unset():unset()只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了。
  • null:将null赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0。
  • 脚本执行结束:该脚本中所有内存都会被释放,无论是否有环引用。

参考文档

有用就打赏一下作者吧!