先看一段代码:
var_dump(memory_get_usage(true)); //int(262144)
$a = str_pad("Hello World",1000000,"Hello World");
var_dump(memory_get_usage(true)); //int(1310720)
$b = $a;
var_dump(memory_get_usage(true)); //int(1310720)
$b = $b.'a';
var_dump(memory_get_usage(true)); //int(2359296)
通过运行结果可以看到(具体数值每个人运行结果可能不一样,这和每台电脑的配置及系统、环境等相关)
1,当给$a赋值时,内存使用量增加了`1310720 - 262144 = 1048576`,我们可以简单理解php存储$a分配了1048576的内存空间
2,当执行 `$b = $a`给$b赋值时内存使用量没变,并没有相应的增加1048576
3,当我们修改$b的值时,内存才增加了`1310720 +1048576 = 2359296`
这就是PHP的写时复制(COW)!
PHP中的COW可以简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时才申请新空间来保存值内容以减少对内存的占用。
写时复制(COW)的应用场景非常多, 比如Linux中对进程复制中内存使用的优化,在各种编程语言中,如C++的STL等等中均有类似的应用。 COW是常用的优化手段,可以归类于:资源延迟分配。只有在真正需要使用资源时才占用资源, 写时复制通常能减少资源的占用。
再来看一段代码:
var_dump(memory_get_usage(true)); //int(262144)
$a = "Hello World";
var_dump(memory_get_usage(true)); //int(262144)
$b = $a;
var_dump(memory_get_usage(true)); //int(262144)
$b = $b.'a';
var_dump(memory_get_usage(true)); //int(262144)
unset($a, $b);
var_dump(memory_get_usage(true)); //int(262144)
这是大家在做实验时经常出现的情况,不管是cow还是unset,内存使用量根本就没变化,为什么呢?
当我们申请内存的时候, PHP并不是简单的根据我们的实际需要多少向OS要多少, 而是会向OS要一个大块的内存, 然后把其中的一块分配给申请者, 这样当再有逻辑来申请内存的时候,如果上次申请的剩余的内存够用就不再需要向OS申请内存了, 避免了频繁的系统调用。同样, 在我们unset释放内存的时候, PHP也不会把内存还给OS, 而会把这块内存, 归入自己维护的空闲内存列表。所以,对于小块内存操作来说, 看起来不管是分配内存还是unset内存,memory_get_usage都不变。
上面说到的cow是怎么来实现的呢,其实就是通过应用计数来实现。
先来说下php变量如何存储的:
1.为变量名分配内存,存入符号表
2.为变量值分配内存
每个php变量存在一个叫"zval"的变量容器中
Zval是PHP中最重要的数据结构之一(另一个比较重要的数据结构是hash table),它包含了PHP中的变量值和类型的相关信息。 Zval结构体定义在Zend/zend.h里面,其结构:
struct _zval_struct {
zvalue_value value; /* 存储变量的值*/
zend_uint refcount__gc; /* 表示引用计数 */
zend_uchar type; /* 变量具体的类型 */
zend_uchar is_ref__gc; /* 表示是否为引用,0:普通变量,1:引用变量 */
};
typedef struct _zval_struct zval;
其中refcount就是引用计数,当给$a赋值时,会开辟zval,refcount为1,赋值给另外一个变量$b,只会增加zval的refcount值,而没有为$b开辟单独的zval,只有在改变$b值是才会复制一份新的zval结构,这就是写时复制的原理,这样做在一定程度上节省内存。
看代码示例:
$a = "i am a";
xdebug_debug_zval('a'); //(refcount=1, is_ref=0),string 'i am a' (length=6)
$b = $a;
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am a' (length=6)
$c = $a;
xdebug_debug_zval('a'); //(refcount=3, is_ref=0),string 'i am a' (length=6)
$b = 'i am b';
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am a' (length=6)
unset($c);
xdebug_debug_zval('a'); //(refcount=1, is_ref=0),string 'i am a' (length=6)
与 标量(scalar)类型的值不同,复合类型变量(array和 object)把它们的成员或属性存在自己的符号表中。除了为变量本身创建一个zval,还会为每个item项生成一个zval容器,垃圾回收部分会做讲解
$a = "i am a";
xdebug_debug_zval('a'); //(refcount=1, is_ref=0),string 'i am a' (length=6)
$b = &$a;
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am a' (length=6)
$b = 'i am b';
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am b' (length=6)
unset($b);
xdebug_debug_zval('a'); //(refcount=1, is_ref=0),string 'i am b' (length=6)
可以看出,在引用变量情况下,改变了$b的值之后并没有触发cow,因为Zend会检查zval的isref是否是引用变量,如果是引用变量,则直接更改zval的value值,否则,需要执行zval分离。由于a 和 b是引用变量,因而更改共享的zval实际上也间接更改了a的值。而在unset(b)之后,变量b从符号表中删除了。
如果一个变量既被复制又被引用,会发生什么呢,能不能共用一个zval结构?
答案是:不可能!
$a = "i am a";
xdebug_debug_zval('a'); //(refcount=1, is_ref=0),string 'i am a' (length=6)
$b = $a;
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am a' (length=6)
$c = &$a;
xdebug_debug_zval('a'); //(refcount=2, is_ref=0),string 'i am a' (length=6)
xdebug_debug_zval('b'); //(refcount=1, is_ref=0),string 'i am a' (length=6)
在$b = $a时,公用了zval,refcount加1,但当$c = &$a时,zval的refcount并没有加1,反而$b被强制复制了,
在这种情况下,变量的值必须分离成两份完全独立的存在!$a与$c共用一个zval,$b自己用一个zval,尽管他们拥有同样的值,但是必须至少通过两个zval来实现。
面试的时候经常被问到的一个问题,在了解垃圾回收机制之前,先得明白什么才叫“垃圾”
1,假设我们使用了一个临时变量$tmp存储了一个字符串,在处理完之后,$tmp变量对于我们来说可以算是一个“垃圾”了,但是对于GC来说,$tmp其实并不是一个垃圾,这个变量实际还存在,$tmp符号依然指向它所对应的zval,GC会认为PHP代码中可能还会使用到此变量,所以不会将其定义为垃圾。
2,那么如果调用unset删除这个变量,那么$tmp是不是就成为一个垃圾了呢。很可惜,GC仍然不认为$tmp是一个垃圾,因为$tmp在unset之后,refcount减少1变成了0,这个时候GC会直接将$tmp对应的zval的内存空间释放,$tmp和其对应的zval就根本不存在了,何来的“垃圾”一说。
垃圾是指变量的容器zval还存在,但是又没有任何变量名指向此zval。
因此垃圾回收器GC(Garbage Collection)判断是否为垃圾的一个重要标准是有没有变量名指向变量容器zval。
先看看在引用计数的时候说过的复合类型变量zval存储:
$arr = ['a' => 'i am a', 'b' => 'i am b'];
xdebug_debug_zval('arr');
/*
(refcount=1, is_ref=0),
array (size=2)
'a' => (refcount=1, is_ref=0),string 'i am a' (length=6)
'b' => (refcount=1, is_ref=0),string 'i am b' (length=6)
*/
$arr['c'] = $arr['a'];
xdebug_debug_zval('arr');
/*
(refcount=1, is_ref=0),
array (size=3)
'a' => (refcount=2, is_ref=0),string 'i am a' (length=6)
'b' => (refcount=1, is_ref=0),string 'i am b' (length=6)
'c' => (refcount=2, is_ref=0),string 'i am a' (length=6)
*/
可以看到,a和c是共用了同一个zval,只是refcount增加了,这符合咱们之前说的引用计数机制。
那如果将数组的引用赋值给数组中的一个元素,会发生什么呢
$arr = ['a' => 'i am a'];
xdebug_debug_zval('arr');
/*
(refcount=1, is_ref=0),
array (size=1)
'a' => (refcount=1, is_ref=0),string 'i am a' (length=6)
*/
$arr['b'] = &$arr;
xdebug_debug_zval('arr');
/*
(refcount=2, is_ref=1),
array (size=2)
'a' => (refcount=1, is_ref=0),string 'i am a' (length=6)
'b' => (refcount=2, is_ref=1),&array<
*/
b元素指向了数组本身,数组refcount加1了,也就是和数组共用了一个zval,这也符合引用计数机制。
但是,如果咱们执行一下 unset($arr),首先$arr从符号表中删除,refcount减一,问题产生了:
$arr已经不在符号表中了,用户无法再访问此变量,但是$arr之前指向的zval的refcount变为1而不是0,因此不能被回收,这样产生了内存泄露!
这样,这么一个zval就成为了一个真是意义的垃圾了,GC要做的工作就是清理这种垃圾。
关于回收算法,官网上有配图及相关四个步骤的解说,说实话比较难理解。我按自己的理解简单的总结一下:
A,把疑似垃圾(refcount减1之后大于0的)的zval放入缓冲区(root buffer),并确保每个zval只出现一次
B,当缓冲区满了,以深度优先对每一个节点所包含的zval进行refcount减1操作。需要强调的是,这个步骤中,起初节点zval本身不做减1操作.
C,算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成垃圾,如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作
D,释放掉上一步骤中标记为垃圾的zval,完成垃圾回收
简单来说就是:对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。
1,unset:unset和释放内存没有关系,它只是断开一个变量到一块内存区域的连接,也就是把引用计数减1;内存是否回收主要还是看refcount是否到0了,以及gc算法判断。所以我上面文章中用到了很多“unset释放内存”的说法其实是错误的,大家理解意思就行,不用较真。
2,null:把变量赋值为null后也没有释放内存,甚至refcount都没有减1,只是修改了zval中类型和值设为null而已
3,脚本执行结束,该脚本中使用的所有内存都会自动被释放,不论是否有引用,所以不需要做额外的内存释放处理
以上说的都是php5的内容管理机制,需要注意的是,php7对内存管理机制做了很大的修改,最重要的改变就是:
1,zval 不再单独从堆上分配内存并且不自己存储引用计数。
2,需要使用 zval 指针的复杂类型(比如数组和对象)会自己存储引用计数。
这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。
这部分我还没仔细研究,一会有时间在把这些补上。