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

概述


PHP中的变量是不需要手动释放的,内核帮我们实现了变量的内存管理,包括内存的分配与回收。

试想一个最简单的自动GC实现方式:在变量被定义时,分配一块内存,用来保存变量的zval和zend_value结构。在函数返回或程序运行结束时,将该变量占用的内存释放掉。在程序执行期间,如果该变量作为参数调用了其他函数或者赋值给其他变量,则把变量复制一份,变量之间相互独立,不会出现冲突。

上述方式可行并且简单。但是有一个无法接受的弊端,那就是效率以及内存严重浪费的问题。比如变量作为参数传递,深拷贝方式会执行内存分配操作,降低了效率,同时在内存中生成两个完全相同的变量,参数在后续如果只有只读操作,那么内存中只需存在一个变量即可,双倍的内存空间浪费了资源。

比较流行的解决方案是:引用计数 + 写时复制,PHP也是基于这两点实现了自动GC机制。
当变量赋值、传递时不是直接进行深拷贝,而是多个变量共用同一个value,引用计数用来记录value有多少个变量在使用;当某个变量的value发生改变时将无法继续与其他变量共用value,这个时候就要进行深拷贝分离value,这就是写时复制。

引用计数


引用计数用来记录当前有多少个zval指向同一个zend_value。当有新的zval指向这个value时,计数器加1;当zval销毁时,计数器减1。当引用计数为0时,表示此value已经没有被任何变量指向,这个时候就可以对value进行释放了。

PHP7将变量的引用计数保存在zend_value中,这一点与之前的PHP版本不同,旧版本中引用计数保存在zva中。PHP7不同类型的结构体中都有一个相同的成员:gc,这个结构就是用来保存引用计数的。

  1. typedef struct _zend_refcounted_h {
  2. uint32_t refcount; /* reference counter 32-bit */
  3. union {
  4. uint32_t type_info;
  5. } u;
  6. } zend_refcounted_h;
  7. struct _zend_refcounted {
  8. zend_refcounted_h gc;
  9. };

并不是所有类型都会用到引用计数,没有具体的value结构的类型是不会用到的,比如整型、浮点型、布尔型、NULL,他们的值直接通过zval保存,因此这些类型不会共用value,而是深拷贝。

写时复制


写时复制的机制在计算机系统中有着非常广泛的应用,它只有在必要的时候(发生写的时候)才会进行深拷贝,可以提升效率。

变量使用引用计数必然会出现其中一个变量修改value的情况,这个时候就需要对value进行分离,发生修改的变量会复制一份数据出来进行修改,同时断开原来value的指向,指向新的value。

并不是所有类型的value都可以进行复制,比如对象、资源就无法复制,也就是无法进行分离,如果多个变量指向同一个对象,当其中一个变量修改对象时,其修改将反映到所有变量上,事实上,只有string、array这两种支持value的分离,与引用计数相同。

回收时机


在自动GC机制中,在zval断开zend_value的指向时,如果发现refcount=0则会直接释放value,这就是变量的回收时机,发生断开的两种常见情况为修改变量与函数返回,修改变量会断开原有value的指向,函数返回时会释放所有的局部变量,也就是把所有局部变量的引用计数减1。

除了自动GC,PHP中也可以通过unset()函数主动销毁一个变量。