内容简介:在平时php-fpm的时候,可能很少人注意php的变量回收,但是到swoole常驻内存开发后,就不得不重视这个了,因为在常驻内存下,如果不了解变量回收机制,可能就会出现内存泄露的问题,本文将一步步带你了解php的垃圾回收机制,让你写出的代码不再内存泄漏首先,php的变量复制用的是写时复制方式,举个例子.
在平时php-fpm的时候,可能很少人注意 php 的变量回收,但是到swoole常驻内存开发后,就不得不重视这个了,因为在常驻内存下,如果不了解变量回收机制,可能就会出现内存泄露的问题,本文将一步步带你了解php的垃圾回收机制,让你写出的代码不再内存泄漏
写时复制
首先,php的变量复制用的是写时复制方式,举个例子.
$a
=
'仙士可'
.time();
$b
=
$a
;
$c
=
$a
;
//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用
$b
=
'仙士可1号'
;
//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间
$a
=
'仙士可2号'
;
//$a的数据发生了变化,同样的,$c也无法引用$a了,需要给$a额外开拓内存空间
详细写时复制可查看:php写时复制
引用计数
既然变量会引用内存,那么删除变量的时候,就会出现一个问题了:
$a
=
'仙士可'
;
$b
=
$a
;
$c
=
$a
;
//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用
$b
=
'仙士可1号'
;
//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间
unset(
$c
);
//这个时候,删除$c,由于$c的数据是引用$a的数据,那么直接删除$a?
很明显,当$c引用$a的时候,删除$c,不能把$a的数据直接给删除,那么该怎么做呢?
这个时候,php底层就使用到了 引用计数 这个概念
引用计数,给变量引用的次数进行计算,当计数不等于0时,说明这个变量已经被引用,不能直接被回收,否则可以直接回收,例如:
$a
=
'仙士可'
.time();
$b
=
$a
;
$c
=
$a
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
xdebug_debug_zval(
'c'
);
$b
=
'仙士可2号'
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
echo
"脚本结束\n"
;
将输出:
a: (refcount=3, is_ref=0)=
'仙士可1578154814'
b: (refcount=3, is_ref=0)=
'仙士可1578154814'
c: (refcount=3, is_ref=0)=
'仙士可1578154814'
a: (refcount=2, is_ref=0)=
'仙士可1578154814'
b: (refcount=1, is_ref=0)=
'仙士可2号'
脚本结束
注意,xdebug_debug_zval函数是xdebug扩展的,使用前必须安装xdebug扩展
引用计数特殊情况
当变量值为整型,浮点型时,在赋值变量时,php7底层将会直接把值存储(php7的结构体将会直接存储简单数据类型),refcount将为0
$a
= 1111;
$b
=
$a
;
$c
= 22.222;
$d
=
$c
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
xdebug_debug_zval(
'c'
);
xdebug_debug_zval(
'd'
);
echo
"脚本结束\n"
;
输出:
a: (refcount=0, is_ref=0)=1111
b: (refcount=0, is_ref=0)=1111
c: (refcount=0, is_ref=0)=22.222
d: (refcount=0, is_ref=0)=22.222
脚本结束
当变量值为interned string字符串型(变量名,函数名,静态字符串,类名等)时,变量值存储在静态区,内存回收被系统全局接管,引用计数将一直为1(php7.3)
$str = '仙士可'; // 静态字符串
$str = '仙士可' . time();//普通字符串
$a
=
'aa'
;
$b
=
$a
;
$c
=
$b
;
$d
=
'aa'
.time();
$e
=
$d
;
$f
=
$d
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'd'
);
echo
"脚本结束\n"
;
输出:
a: (refcount=1, is_ref=0)=
'aa'
d: (refcount=3, is_ref=0)=
'aa1578156506'
脚本结束
当变量值为以上几种时,复制变量将会直接拷贝变量值,所以将不存在多次引用的情况
引用时引用计数变化
如下代码:
$a
=
'aa'
;
$b
= &
$a
;
$c
=
$b
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
xdebug_debug_zval(
'c'
);
echo
"脚本结束\n"
;
将输出:
a: (refcount=2, is_ref=1)=
'aa'
b: (refcount=2, is_ref=1)=
'aa'
c: (refcount=1, is_ref=0)=
'aa'
脚本结束
当引用时,被引用变量的value以及类型将会更改为引用类型,并将引用值指向原来的值内存地址中.
之后引用变量的类型也会更改为引用类型,并将值指向原来的值内存地址,这个时候,值内存地址被引用了2次,所以refcount=2.
而$c并非是引用变量,所以将值复制给了$c,$c引用还是为1
详细引用计数知识,底层原理可查看:https://www.cnblogs.com/sohuhome/p/9800977.html
php生命周期
php将每个运行域作为一次生命周期,每次执行完一个域,将回收域内所有相关变量:
<?php
/**
* Created by PhpStorm.
* User: Tioncico
* Date: 2020/1/6 0006
* Time: 14:22
*/
echo
"php文件的全局开始\n"
;
class
A{
protected
$a
;
function
__construct(
$a
)
{
$this
->a =
$a
;
echo
"类A{$this->a}生命周期开始\n"
;
}
function
test(){
echo
"类test方法域开始\n"
;
echo
"类test方法域结束\n"
;
}
//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
function
__destruct()
{
echo
"类A{$this->a}生命周期结束\n"
;
// TODO: Implement __destruct() method.
}
}
function
a1(){
echo
"a1函数域开始\n"
;
$a
=
new
A(1);
echo
"a1函数域结束\n"
;
//函数结束,将回收所有在函数a1的变量$a
}
a1();
$a
=
new
A(2);
echo
"php文件的全局结束\n"
;
//全局结束后,会回收全局的变量$a
可看出,每个方法/函数都作为一个作用域,当运行完该作用域时,将会回收这里面的所有变量.
再看看这个例子:
echo
"php文件的全局开始\n"
;
class
A
{
protected
$a
;
function
__construct(
$a
)
{
$this
->a =
$a
;
echo
"类{$this->a}生命周期开始\n"
;
}
function
test()
{
echo
"类test方法域开始\n"
;
echo
"类test方法域结束\n"
;
}
//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
function
__destruct()
{
echo
"类{$this->a}生命周期结束\n"
;
// TODO: Implement __destruct() method.
}
}
$arr
= [];
$i
= 0;
while
(1) {
$arr
[] =
new
A(
'arr_'
.
$i
);
$obj
=
new
A(
'obj_'
.
$i
);
$i
++;
echo
"数组大小:"
.
count
(
$arr
).
'\n'
;
sleep(1);
//$arr 会随着循环,慢慢的变大,直到内存溢出
}
echo
"php文件的全局结束\n"
;
//全局结束后,会回收全局的变量$a
全局变量只有在脚本结束后才会回收,而在这份代码中,脚本永远不会被结束,也就说明变量永远不会回收,$arr还在不断的增加变量,直到内存溢出.
内存泄漏
请看代码:
function
a(){
class
A {
public
$ref
;
public
$name
;
public
function
__construct(
$name
) {
$this
->name =
$name
;
echo
(
$this
->name.
'->__construct();'
.PHP_EOL);
}
public
function
__destruct() {
echo
(
$this
->name.
'->__destruct();'
.PHP_EOL);
}
}
$a1
=
new
A(
'$a1'
);
$a2
=
new
A(
'$a2'
);
$a3
=
new
A(
'$3'
);
$a1
->ref =
$a2
;
$a2
->ref =
$a1
;
unset(
$a1
);
unset(
$a2
);
echo
(
'exit(1);'
.PHP_EOL);
}
a();
echo
(
'exit(2);'
.PHP_EOL);
当$a1和$a2的属性互相引用时,unset($a1,$a2) 只能删除变量的引用,却没有真正的删除类的变量,这是为什么呢?
首先,类的实例化变量分为2个步骤,1:开辟类存储空间,用于存储类数据,2:实例化一个变量,类型为class,值指向类存储空间.
当给变量赋值成功后,类的引用计数为1,同时,a1->ref指向了a2,导致a2类引用计数增加1,同时a1类被a2->ref引用,a1引用计数增加1
当unset时,只会删除类的变量引用,也就是-1,但是该类其实还存在了一次引用(类的互相引用),
这将造成这2个类内存永远无法释放,直到被gc机制循环查找回收,或脚本终止回收(域结束无法回收).
手动回收机制
在上面,我们知道了 脚本回收,域结束回收2种php回收方式 ,那么可以手动回收吗?答案是可以的.
手动回收有以下几种方式:
unset,赋值为null,变量赋值覆盖,gc_collect_cycles函数回收
unset
unset为最常用的一种回收方式,例如:
class
A
{
public
$ref
;
public
$name
;
public
function
__construct(
$name
)
{
$this
->name =
$name
;
echo
(
$this
->name .
'->__construct();'
. PHP_EOL);
}
public
function
__destruct()
{
echo
(
$this
->name .
'->__destruct();'
. PHP_EOL);
}
}
$a
=
new
A(
'$a'
);
$b
=
new
A(
'$b'
);
unset(
$a
);
//a将会先回收
echo
(
'exit(1);'
. PHP_EOL);
//b需要脚本结束才会回收
输出:
$a->__construct();
$b->__construct();
$a->__destruct();
exit
(1);
$b->__destruct();
unset的回收原理其实就是引用计数-1,当引用计数-1之后为0时,将会直接回收该变量,否则不做操作(这就是上面内存泄漏的原因,引用计数-1并没有等于0)
=null回收
class
A
{
public
$ref
;
public
$name
;
public
function
__construct(
$name
)
{
$this
->name =
$name
;
echo
(
$this
->name .
'->__construct();'
. PHP_EOL);
}
public
function
__destruct()
{
echo
(
$this
->name .
'->__destruct();'
. PHP_EOL);
}
}
$a
=
new
A(
'$a'
);
$b
=
new
A(
'$b'
);
$c
=
new
A(
'$c'
);
unset(
$a
);
$c
=null;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
xdebug_debug_zval(
'c'
);
echo
(
'exit(1);'
. PHP_EOL);
=null和unset($a),作用其实都为一致,null将变量值赋值为null,原先的变量值引用计数-1,而unset是将变量名从php底层变量表中清理,并将变量值引用计数-1,唯一的区别在于,=null,变量名还存在,而unset之后,该变量就没了:
$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a: no such symbol
//
$a已经不在符号表
b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)=
'$b'
}
c: (refcount=0, is_ref=0)=NULL
//c
还存在,只是值为null
exit
(1);
$b->__destruct();
变量覆盖回收
通过给变量赋值其他值(例如null)进行回收:
class
A
{
public
$ref
;
public
$name
;
public
function
__construct(
$name
)
{
$this
->name =
$name
;
echo
(
$this
->name .
'->__construct();'
. PHP_EOL);
}
public
function
__destruct()
{
echo
(
$this
->name .
'->__destruct();'
. PHP_EOL);
}
}
$a
=
new
A(
'$a'
);
$b
=
new
A(
'$b'
);
$c
=
new
A(
'$c'
);
$a
=null;
$c
=
'练习时长两年半的个人练习生'
;
xdebug_debug_zval(
'a'
);
xdebug_debug_zval(
'b'
);
xdebug_debug_zval(
'c'
);
echo
(
'exit(1);'
. PHP_EOL);
将输出:
$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a: (refcount=0, is_ref=0)=NULL
b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)=
'$b'
}
c: (refcount=1, is_ref=0)=
'练习时长两年半的个人练习生'
exit
(1);
$b->__destruct();
可以看出,c由于覆盖赋值,将原先A类实例的引用计数-1,导致了$c的回收,但是从程序的内存占用来说,覆盖变量并不是意义上的内存回收,只是将变量的内存修改为了其他值.内存不会直接清空.
gc_collect_cycles
回到之前的内存泄漏章节,当写程序不小心造成了内存泄漏,内存越来越大,可是php默认只能脚本结束后回收,那该怎么办呢?我们可以使用 gc_collect_cycles 函数,进行手动回收
function
a(){
class
A {
public
$ref
;
public
$name
;
public
function
__construct(
$name
) {
$this
->name =
$name
;
echo
(
$this
->name.
'->__construct();'
.PHP_EOL);
}
public
function
__destruct() {
echo
(
$this
->name.
'->__destruct();'
.PHP_EOL);
}
}
$a1
=
new
A(
'$a1'
);
$a2
=
new
A(
'$a2'
);
$a1
->ref =
$a2
;
$a2
->ref =
$a1
;
$b
=
new
A(
'$b'
);
$b
->ref =
$a1
;
echo
(
'$a1 = $a2 = $b = NULL;'
.PHP_EOL);
$a1
=
$a2
=
$b
= NULL;
echo
(
'gc_collect_cycles();'
.PHP_EOL);
echo
(
'// removed cycles: '
.gc_collect_cycles().PHP_EOL);
//这个时候,a1,a2已经被gc_collect_cycles手动回收了
echo
(
'exit(1);'
.PHP_EOL);
}
a();
echo
(
'exit(2);'
.PHP_EOL);
输出:
$a1->__construct();
$a2->__construct();
$b->__construct();
$a1 = $a2 = $b = NULL;
$b->__destruct();
gc_collect_cycles();
$a1->__destruct();
$a2->__destruct();
//
removed cycles: 4
exit
(1);
exit
(2);
注意,gc_colect_cycles 函数会从php的符号表,遍历所有变量,去实现引用计数的计算并清理内存,将消耗大量的cpu资源,不建议频繁使用
另外,除去这些方法,php内存到达一定临界值时,会自动调用内存清理(我猜的),每次调用都会消耗大量的资源,可通过 gc_disable 函数,去关闭php的自动gc
其他
以上就是全部内容了,如果发现文章有错,希望指出,也可以加我好友互相讨论
----------伟大的分割线-----------
PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!
饭米粒只发原创或授权发表的文章,不转载网上的文章
所发的文章,均可找到原作者进行沟通。
也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投 稿 。
投稿请联系:
shenzhe163@gmail.com
本文由 仙士可 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
松本行弘的程式世界
松本行弘 / 鄧瑋敦 / 博碩 / 2010年07月27日
讓Ruby之父教您大師級的程式思考術! 本書以松本行弘先生對程式本質的深層認知、各種技術之優缺點的掌握,闡述Ruby這套程式語言的設計理念,並由此延伸讓您一窺程式設計的奧妙之處。本書內含許多以Ruby、Lisp、Smalltalk、Erlang、JavaScript等動態語言所寫成的範例,從動態語言、函數式程式設計等領域開展您的學習視野。 本書精華: ‧物件導向與抽象化 ‧......一起来看看 《松本行弘的程式世界》 这本书的介绍吧!