C/C++语言中,如果想在堆上分配变量,需要手动进行内存的分配与释放,变量的内存管理是一件非常烦琐的事情,稍有不慎就可能导致不可预知的错误。现代高级语言普遍提供了变量的自动GC机制,由语言自己进行管理,这使得开发者不需要再去关心变量的分配与释放,将开发者从内存管理的苦海中解脱出来。PHP同样实现了这种机制,在PHP中可以直接通过“$”声明一个变量,使用完也不需要手动销毁,内核自己清楚什么时间该进行释放。
我们先自己思考下如何实现自动GC,最简单的实现方式:在函数中定义变量时分配一块内存,用于保存zval及对应的value结构,在函数返回时再将内存释放,如果在函数执行期间该变量作为参数调用了其他函数或者赋值给了其他变量,则把变量复制一份,变量之间互相独立,不会出现冲突。
这种方式是可行的,而且内存管理也很简单,但是,深拷贝带来的一个无法接受的问题是效率,而且内存浪费严重,比如我们定义了一个变量然后赋值给另外一个变量,可能后面都只是只读操作,假如深拷贝的话就会有多余的一份数据。这个问题比较通用的解决方案是:引用计数+写时复制,PHP变量的内存管理正是基于这两点实现的。当变量赋值、传递时不是直接进行深拷贝,而是多个变量共用同一个value,引用计数用来记录value有多少个变量在使用;当某个变量的value发生改变时将无法继续与其他变量共用value,这个时候就需要进行深拷贝分离value,这就是写时复制。
PHP 7提供了比较节省内存的赋值操作,字符串在赋值时并不直接拷贝一份数据,而是进行zend_string中的refcount++,字符串销毁时再进行zend_string中的refcount–。
一般认为,当字符串进行赋值操作时,对应字符串会refcount++,但实际情况却并不总是如此,这里一一举例罗列出来了每种情况。
- 当字符串是常量字符串时,赋值只修改zval中str的指针地址,两个字符串指向同一个str地址,但是refcount的值始终都是0。字符串的gc.flags会被标识成2。$b = ‘time:’.time(); “time:”为常量字符串,’time:’.time()计算返回的字符串为临时字符串
- 当字符串的值不是一个常量字符串时,每次赋值会执行字符串的refcount++。临时字符串的gc.flags被标识成0。$a = ‘hello’;就是一个常量字符串
- 当字符串是整数时,值直接存储在zval结构体中,并无引用计数的变更,赋值操作是直接把$a的值拷贝到了$b的zval.lval字段中,因值存储少了一个zend_string结构体,相比字符串更省内存。因为有这样一个误区——字符串的赋值用了引用,实际上只有一份数据,而整型数据赋值直接拷贝,有多份数据,所以后者更占内存。这样的说法其实是错误的。
- 当赋值为引用类型时$a与$b的关系会如何呢?引用赋值时,会多出zend_reference结构体,里面包含gc及zval字段,赋值时gc进行refcount++,字符串的引用赋值和其他类型引用赋值的实现方式都是一样的。
查看第一种情况:首先,我们先看一下我们的源代码
接着,我们利用vld扩展查看最终生成的opcode
根据生成的中间代码和OP1和OP2,我们会在以下地方打上断点,然后开始run。
一直输入n,我们看到了以下代码
这时候就表示变量赋值成功,那么我们查看一下value里面的值。我们可以看到u1.v.type=6,表示类型为字符串,那么对应的值就存在value.str里面。
最后我们也看到了hello这个值。这就表示$a=’hello’这行代码已经执行完了,那么我们再接着输入n往下看。
我们可以对比两个图,我们发现两张图的value.str的值是一样的。说明$a,$b这两个变量引用的是同一块地址值。 可能有人觉得上面两张图是一样的,其实仔细看p value这行可以发现两个图中显示的地址是不一样的。到了这一步,$b=$a这一行也执行完了,那么我们再接着输入n往下看。
我们可以看到最后输出的值是test,并且value.str的地址和以前的不一样了,说明此时$a和$b的值存在不同的地址块了。
那么我们简单的看下第2种临时字符串的情况。我们就不详细解释,源码:
vld查看opcode
打上断点,开始调试。
输入n
我们可以看到refcount=0,flags=0,符合情况1。我们再接着输入n,继续往下看。
我们可以看到refcount=2,说明在临时字符串这种情况下,每次引用赋值,refcount都会加1。
接着看第三种情况,源码
vld查看opcode
打上断点,开始调试。
一直输入n,查看第一个赋值操作
我们可以看到value.lval=10,value.u1.v.type=4,type=4表示是数值类型,表示对应的值存在value.lval里。接着我们看第二个赋值操作。接着往下输入n。
由此也证明了情况3的说法是正确的。
接着看情况4,首先我们查看源码:
vld查看opcode
打上断点,开始调试。
一直输入n,查看第一个赋值操作,我们可以看到u1.v.type=6,那么我们知道其对应的值存在value.str里面
我们再接着看第二个赋值操作。,我们看到了u.v1.type=10,表示是一个引用类型,表示值是存在value.ref里面的。接着我们去value.ref里面查看了,发现其对应的u1.v.type=6,表示是一个字符串类型,表示值是存在value.str里面的。最后我们也确实在value.str里面看到的对应的值。
参考书籍:
《PHP7内核剖析》
《PHP底层设计与源码实现》