本章内容主要来源于秦朋先生编著的《PHP7内核剖析》这本书籍,感兴趣的小伙伴可以购买该书,进行系统性学习。

概述


通过引用计数,PHP实现了变量的自动GC机制,但是有一种情况是这个机制无法解决的,从而因变量无法回收导致内存始终得不到释放,造成内存泄漏,这种情况指的是循环引用。简单的讲就是变量的内部成员引用了变量自身,比如数组中的某个元素指向了数组,这样一来,数组的引用计数中就有一个来自自身成员,当所有的外部引用全部断开时,数组的refcount仍然大于0而得不到释放,而实际上这种变量不可能再被使用了。

  1. $a = array(1);
  2. $a[] = &$a;
  3. 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中。

  1. // PHP 7.0.12
  2. typedef struct _gc_root_buffer {
  3. zend_refcounted *ref;
  4. struct _gc_root_buffer *next; /* double-linked list */
  5. struct _gc_root_buffer *prev;
  6. uint32_t refcount;
  7. } gc_root_buffer;
  8. typedef struct _zend_gc_globals {
  9. zend_bool gc_enabled;
  10. zend_bool gc_active;
  11. zend_bool gc_full;
  12. gc_root_buffer *buf; /* preallocated arrays of buffers */
  13. gc_root_buffer roots; /* list of possible roots of cycles */
  14. gc_root_buffer *unused; /* list of unused buffers */
  15. gc_root_buffer *first_unused; /* pointer to first unused buffer */
  16. gc_root_buffer *last_unused; /* pointer to last unused buffer */
  17. gc_root_buffer to_free; /* list to free */
  18. gc_root_buffer *next_to_free;
  19. uint32_t gc_runs;
  20. uint32_t collected;
  21. #if GC_BENCH
  22. uint32_t root_buf_length;
  23. uint32_t root_buf_peak;
  24. uint32_t zval_possible_root;
  25. uint32_t zval_buffered;
  26. uint32_t zval_remove_from_buffer;
  27. uint32_t zval_marked_grey;
  28. #endif
  29. } zend_gc_globals;
  30. // PHP 7.4.15
  31. typedef struct _gc_root_buffer {
  32. zend_refcounted *ref;
  33. } gc_root_buffer;
  34. typedef struct _zend_gc_globals {
  35. gc_root_buffer *buf; /* preallocated arrays of buffers */
  36. zend_bool gc_enabled;
  37. zend_bool gc_active; /* GC currently running, forbid nested GC */
  38. zend_bool gc_protected; /* GC protected, forbid root additions */
  39. zend_bool gc_full;
  40. uint32_t unused; /* linked list of unused buffers */
  41. uint32_t first_unused; /* first unused buffer */
  42. uint32_t gc_threshold; /* GC collection threshold */
  43. uint32_t buf_size; /* size of the GC buffer */
  44. uint32_t num_roots; /* number of roots in GC buffer */
  45. uint32_t gc_runs;
  46. uint32_t collected;
  47. #if GC_BENCH
  48. uint32_t root_buf_length;
  49. uint32_t root_buf_peak;
  50. uint32_t zval_possible_root;
  51. uint32_t zval_buffered;
  52. uint32_t zval_remove_from_buffer;
  53. uint32_t zval_marked_grey;
  54. #endif
  55. } 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设置是否开启,默认是开启的。

(后续进行源码分析)