GC

简介:PHP 是一门托管型语言,在 PHP 编程中,程序员不需要手工处理内存资源的分配与释放(使用 C 编写 PHP 或 Zend 扩展除外),这就意味着 PHP 本身实现了垃圾回收机制(Garbage Collection)。在 PHP 官方网站可以看到对垃圾回收机制的介绍,在开始介绍垃圾回收之前我们先介绍几个关键的概念。


PHP的引用计数:(refcount,is_ref)

PHP在内核中是通过zval这个结构体来存储变量的,在Zend/zend.h文件中找到了其定义:

PHP5 中定义如下:

struct_zval_struct {

zvalue_valuevalue;

zend_uint refcount;        

zend_uchar type;/* active type */

zend_uchar is_ref;

};


一个例子:

第一步:查看内部结构

$a = 'zd';  

我们可以使用xdebug查看zval的信息

xdebug_debug_zval('a'); 

存储的内部信息是 name:(refcount=1, is_ref=0)='zd' 

说明php通过zavl的refcount变量来存储引用计数

第二步:增加一个计数

$a = 'zd';   $b = $a;

xdebug_debug_zval('a'); 

存储的内部信息是: name:(refcount=2, is_ref=0),string'zd' 

证明这种情况下并不会开辟两块内存空间,并且refcount会加1

第三步骤:引用赋值

当我们使用值引用时,$a = 'zd';   $b = &$a;

xdebug_debug_zval('a'); 

存储的内部信息是:name:(refcount=2, is_ref=1)='zd'

所以我们可以的得到的信息是:引用赋值会导致zval通过is_ref来标记是否存在引用的情况。

第四步:数组型的变量

$test = ['a'=>'1','b'=>'2']; 

xdebug_debug_zval('test');

test: (refcount=1, is_ref=0)=array ( 

   'a' => (refcount=1, is_ref=0)='1', 

    'b' => (refcount=1,is_ref=0)='2' 

)

由此我们可以明白:数组内部的元素也会生成对应的zavl,上述的例子存在3个zval,这3个zval都遵循变量的引用和计数原则。

第五步:unset掉变量,使refcount减1

$a = 'zd';

unset($a);

xdebug_debug_zval('a');

a:(refcount=0, is_ref=0)='zd'  


上面提到的refcount=0的情况,会被自动的销毁掉,其实并不属于垃圾,下面我们来想一想为什么是垃圾?为什么要有垃圾回收呢,如果不回收会引入什么问题呢?

PHP5.3 之前的内存泄漏的垃圾回收

产生内存泄漏主要真凶:环形引用。 现在来造一个环形引用的场景:

$a= ['one'];

$a[] = &$a;

xdebug_debug_zval('a');

a: (refcount=2, is_ref=1)=array (

        0 => (refcount=1, is_ref=0)='one',

        1 => (refcount=2, is_ref=1)=…

)

“…”表示1指向a自身,是一个环形引用

环形引用

这个时候我们对$a进行unset,那么$a会从符号表中删除,同时$a指向的zval的refcount减少1,那么问题也就产生了,$a已经不在符号表中了,用户无法再访问此变量,但是$a之前指向的zval的refcount由2变为1而不是0,因此不能被回收,这样产生了内存泄露:


为解决这种垃圾,产生了新的GC

        在较新的PHP手册中有简单的介绍新的GC使用的垃圾清理算法,这个算法名为 Concurrent Cycle Collection in Reference Counted Systems , 这里不详细介绍此算法,根据手册中的内容来先简单的介绍一下思路:

首先我们有几个基本的准则:

1:如果一个zval的refcount增加,那么此zval还在使用,不属于垃圾

2:如果一个zval的refcount减少到0, 那么zval可以被释放掉,不属于垃圾

3:如果一个zval的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾

只有在准则3下,GC才会把zval收集起来,然后通过新的算法来判断此zval是否为垃圾。那么如何判断这么一个变量是否为真正的垃圾呢?简单的说,就是对此zval中的每个元素进行一次refcount减1操作,操作完成之后,如果zval的refcount=0,那么这个zval就是一个垃圾。这个原理咋看起来很简单,但是又不是那么容易理解,起初笔者也无法理解其含义,直到挖掘了源代码之后才算是了解。如果你现在不理解没有关系,后面会详细介绍,这里先把这算法的几个步骤描叙一下,首先引用手册中的一张图:


垃圾回收的过程

A:为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,此算法会先把所有前面准则3情况下的zval节点放入一个节点(root)缓冲区(root buffer),并且将这些zval节点标记成紫色,同时算法必须确保每一个zval节点在缓冲区中之出现一次。当缓冲区被节点塞满的时候,GC才开始开始对缓冲区中的zval节点进行垃圾判断。

B:当缓冲区满了之后,算法以深度优先对每一个节点所包含的zval进行减1操作,为了确保不会对同一个zval的refcount重复执行减1操作,一旦zval的refcount减1之后会将zval标记成灰色。需要强调的是,这个步骤中,起初节点zval本身不做减1操作,但是如果节点zval中包含的zval又指向了节点zval(环形引用),那么这个时候需要对节点zval进行减1操作。

C:算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成白色(代表垃圾),如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作,同时将这些zval的颜色变成黑色(zval的默认颜色属性)

D:遍历zval节点,将C中标记成白色的节点zval释放掉。

这ABCD四个过程是手册中对这个算法的介绍,这还不是那么容易理解其中的原理,这个算法到底是个什么意思呢?我自己的理解是这样的:

        比如还是前面那个变成垃圾的数组$a对应的zval,命名为zval_a,  如果没有执行unset, zval_a的refcount为2,分别由$a和$a中的索引1指向这个zval。  

        用算法对这个数组中的所有元素(索引0和索引1)的zval的refcount进行减1操作,由于索引1对应的就是zval_a,所以这个时候zval_a的refcount应该变成了1,这样zval_a就不是一个垃圾。

        如果执行了unset操作,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用算法对数组中的所有元素(索引0和索引1)的zval的refcount进行减1操作,这样zval_a的refcount就会变成0,于是就发现zval_a是一个垃圾了。 算法就这样发现了顽固的垃圾数据。

举了这个例子,读者大概应该能够知道其中的端倪:

对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。

这个道理其实很简单,假设数组a的refcount等于m, a中有n个元素又指向a,如果m等于n,那么算法的结果是m减n,m-n=0,那么a就是垃圾,如果m>n,那么算法的结果m-n>0,所以a就不是垃圾了

m=n代表什么?  代表a的refcount都来自数组a自身包含的zval元素,代表a之外没有任何变量指向它,代表用户代码空间中无法再访问到a所对应的zval,代表a是泄漏的内存,因此GC将a这个垃圾回收了。


PHP中运用新的GC的算法

在PHP中,GC默认是开启的,你可以通过ini文件中的zend.enable_gc 项来开启或则关闭GC。当GC开启的时候,垃圾分析算法将在节点缓冲区(roots buffer)满了之后启动。缓冲区默认可以放10,000个节点,当然你也可以通过修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 来改变这个数值,需要重新编译链接PHP。

当GC关闭的时候,垃圾分析算法就不会运行,但是相关节点还会被放入节点缓冲区,这个时候如果缓冲区节点已经放满,那么新的节点就不会被记录下来,这些没有被记录下来的节点就永远也不会被垃圾分析算法分析。如果这些节点中有循环引用,那么有可能产生内存泄漏。

之所以在GC关闭的时候还要记录这些节点,是因为简单的记录这些节点比在每次产生节点的时候判断GC是否开启更快,另外GC是可以在脚本运行中开启的,所以记录下这些节点,在代码运行的某个时候如果又开启了GC,这些节点就能被分析算法分析。当然垃圾分析算法是一个比较耗时的操作。

    在PHP代码中我们可以通过gc_enable()和gc_disable()函数来开启和关闭GC,也可以通过调用gc_collect_cycles()在节点缓冲区未满的情况下强制执行垃圾分析算法。这样用户就可以在程序的某些部分关闭或则开启GC,也可强制进行垃圾分析算法。


1.unset函数

    unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;内存是否回收主要还是看refount是否到0了,以及gc算法判断。

2.= null 操作;

    a=null是直接将a 指向的数据结构置空,同时将其引用计数归0。

3.脚本执行结束

    脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 每一种语言都有自己的自动垃圾回收机制,让程序员不必过分关心程序内存分配,但是在OOP中,有些对象需要显式的销毁;防...
    文档随手记阅读 4,664评论 2 3
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,488评论 3 83
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,927评论 2 31
  • G1 GC知识点: Region:1M~64M,2的幂,默认为其大小为将堆分为约2048个region为宜。可以通...
    阳丶小光阅读 4,807评论 0 7
  • JVM内存区域 了解Java GC之前,必须先搞清楚JVM中内存区域的划分。 JVM中内存区域大致可分为如上图所示...
    特立独行的猪手阅读 2,599评论 0 13