本章内容主要来源于秦朋先生编著的《PHP7内核剖析》这本书籍,感兴趣的小伙伴可以购买该书,进行系统性学习。
概述
通过引用计数,PHP实现了变量的自动GC机制,但是有一种情况是这个机制无法解决的,从而因变量无法回收导致内存始终得不到释放,造成内存泄漏,这种情况指的是循环引用。简单的讲就是变量的内部成员引用了变量自身,比如数组中的某个元素指向了数组,这样一来,数组的引用计数中就有一个来自自身成员,当所有的外部引用全部断开时,数组的refcount仍然大于0而得不到释放,而实际上这种变量不可能再被使用了。
$a = array(1);
$a[] = &$a;
unset($a);
unset之前,变量$a的类型为引用,该引用的refcount=2,一个来自$a,另一个来自$a[1]。
unset之后,减少了一次该引用的refcount,此时已经没有任何的外部引用,但是数组中仍然有一个元素指向该引用。
这种因为循环引用而导致的无法释放的变量称之为垃圾。
回收机制
PHP引用另外一种机制来对这些垃圾进行回收,也就是垃圾回收器。
- 如果一个变量value的refcount减少到0,那么此value可以被释放掉,不属于垃圾。
- 如果一个变量value的refcount减少之后大于0,那么此value还不能被释放,此value可能成为一个垃圾。
对于可能成为垃圾的变量会被垃圾回收器收集起来,等达到一定数量后开始启动垃圾鉴定程序,把真正的垃圾释放掉。
目前垃圾只会出现在array、object这两种类型中,其他类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收器只会处理这两种类型。垃圾回收器判断是否要收集疑似垃圾时,并不是根据类型进行判断的,而是通过zval.u1.type_flag进行标识的,只有包含IS_TYPE_COLLECTABLE标识的变量类型才会被收集。
垃圾回收器把收集到的可能垃圾垃圾保存到一个buffer缓存区中,收集的时机是refcount减少时,也就是每次refcount减少时都会试图收集,但发现已经收集后就不再重复收集。
回收算法
垃圾回收器收集的疑似垃圾达到一定数量后,就会启动垃圾鉴定、回收程序。回收算法的原理很简单:既然垃圾是由于成员引用自身导致的,那么就对value的所有成员减一遍引用计数,结果如果发现value本身refcount变为0,则就表明其引用全部来自自身成员,具体的回收过程:
- 遍历垃圾回收器的buffer缓存区,把当前value标为灰色(GC_GREY),然后对当前value的成员进行深度优先遍历,把成员value的refcount减1,并且也标为灰色。
- 重复遍历buffer,检查当前value引用是否为0,为0则表示确实是垃圾,把它标为白色(GC_WHITE),如果不为0,则排除了引用全部来自自身成员的可能,表示还有外部的引用,并不是垃圾,这时候因为第一步对成员进行了refcount减1操作,需要再还原回去,对所有成员进行深度遍历,把成员refcount加1,同时标为黑色。
- 再次遍历buffer,将非GC_WHITE的节点从buffer中删除,最终buffer缓存区中全部为真正的垃圾,最后将这些垃圾释放,回收完成。
垃圾缓存区
垃圾回收器主要通过zend_gc_globals这个结构对垃圾进行管理,收集到的疑似垃圾的value就保存在这个结构的buf中。
// PHP 7.0.12
typedef struct _gc_root_buffer {
zend_refcounted *ref;
struct _gc_root_buffer *next; /* double-linked list */
struct _gc_root_buffer *prev;
uint32_t refcount;
} gc_root_buffer;
typedef struct _zend_gc_globals {
zend_bool gc_enabled;
zend_bool gc_active;
zend_bool gc_full;
gc_root_buffer *buf; /* preallocated arrays of buffers */
gc_root_buffer roots; /* list of possible roots of cycles */
gc_root_buffer *unused; /* list of unused buffers */
gc_root_buffer *first_unused; /* pointer to first unused buffer */
gc_root_buffer *last_unused; /* pointer to last unused buffer */
gc_root_buffer to_free; /* list to free */
gc_root_buffer *next_to_free;
uint32_t gc_runs;
uint32_t collected;
#if GC_BENCH
uint32_t root_buf_length;
uint32_t root_buf_peak;
uint32_t zval_possible_root;
uint32_t zval_buffered;
uint32_t zval_remove_from_buffer;
uint32_t zval_marked_grey;
#endif
} zend_gc_globals;
// PHP 7.4.15
typedef struct _gc_root_buffer {
zend_refcounted *ref;
} gc_root_buffer;
typedef struct _zend_gc_globals {
gc_root_buffer *buf; /* preallocated arrays of buffers */
zend_bool gc_enabled;
zend_bool gc_active; /* GC currently running, forbid nested GC */
zend_bool gc_protected; /* GC protected, forbid root additions */
zend_bool gc_full;
uint32_t unused; /* linked list of unused buffers */
uint32_t first_unused; /* first unused buffer */
uint32_t gc_threshold; /* GC collection threshold */
uint32_t buf_size; /* size of the GC buffer */
uint32_t num_roots; /* number of roots in GC buffer */
uint32_t gc_runs;
uint32_t collected;
#if GC_BENCH
uint32_t root_buf_length;
uint32_t root_buf_peak;
uint32_t zval_possible_root;
uint32_t zval_buffered;
uint32_t zval_remove_from_buffer;
uint32_t zval_marked_grey;
#endif
} zend_gc_globals;
分析PHP 7.0.12
- gc_enable:是否启用GC
- gc_active:是否在垃圾检查过程中
- gc_full:缓存区是否已满
- *buf:垃圾缓存区
- roots:buf中最新加入的一个疑似垃圾
- *unused:buf中没有使用的buffer
- *first_unused:buf中第一个没有使用的buffer
- *last_unused:buf的尾部
- to_free、next_to_free:待释放垃圾
- gc_runs:统计gc运行次数
- collected:统计已回收的垃圾数
buf用于保存收集到的value,他是一个数组,在垃圾回收器初始化一次性分配了10001个gc_root_buffer,其中第一个buffer被保留,插入value时直接取出可用节点即可。roots指向buf中最新加入的一个节点,roots是一个双向链表的头部,之所以是一个双向链表,是因为buf数组中保存的只是有可能成为垃圾的value,其中有些value在加入之后又被删除了(比如有的value在之后的操作中refcount变为0了,这个时候就需要从buf中删除了),这样buf数组中就会出现一些空隙。first_unused一开始指向buf的第一个位置,有元素插入roots时如果first_unused还没有到达buf的尾部,则返回first_buffer给最新的元素,然后执行first_unused++,直到last_unused。
unused成员,它的含义与first_unused类似,用来管理buf中开始加入后面又删除的节点,这是一个单链表。也就是说,first_unused是一只往后偏移的,直到buf的结尾,buf中间由于value删除而重新空闲的节点则由unused串起来。下次有新的value插入roots时优先使用unused的这些节点,其次才是first_unused的节点。
垃圾回收机制可以通过php.ini中的zend.enable_gc设置是否开启,默认是开启的。
(后续进行源码分析)