PHP 的垃圾回收机制

垃圾回收,简称 gc。顾名思义,就是废物重利用的意思。再说这个之前先接触一下内存泄露,大概意思就是申请了一块地儿拉了会儿屎,拉完后不收拾,那么那块儿地就算是糟蹋了,地越用越少,最后一地全是屎。说到底一句,用了记得还。一定程度上说,垃圾回收机制就是用来擦屁股的。如果用过 C 语言,那么申请内存的方式是 malloc 或者是 calloc,然后你用完这个内存后,一定不要忘了用 free 函数去释放掉,这就是传说中手动垃圾回收,一般都是扫地神僧用这种方式。很多高层次语言中,你这辈子都是接触不到内存管理的,比如世界上最好的语言 php,这种语言替你管理了内存,你就安安心心写烂代码即可。写 php 的,你说你关心内存,我是不怎么相信的,一定是你在装逼。当然了,如果你用的 swoole 或者 wm 或者自己发明的常驻内存级 php 应用, 那你将不得不关注内存泄露问题,也就说一定要记得释放无用变量。那么,在用的最普遍地最传统的 web 开发中,php 的自动垃圾回收机制是怎样的呢?这个问题我们先这么想,就是都知道 php 是 C 语言实现的,现在把 C 语言给你放在这里了,然后你想想如何用 C 语言实现对一个变量的统计以及释放。你不要想如何实现 php,你就想 C 语言如何实现一个变量,从声明开始到最后没人用了,就把这个变量所占的内存给释放掉。你从这个角度出发,就会舒服一些,这不再是一个技术难题,而是一个傻逼产品经理提的一个傻逼需求。好了,步入正题,PHP 进行内存管理的核心算法一共两项:一是引用计数,二是写时拷贝,请理(bei)解(song)。当你声明一个 PHP 变量的时候,C 语言就在底层给你搞了一个叫做 zval 的 struct(结构体);如果你还给这个变量赋值了,比如 “hello world”,那么 C 语言就在底层再给你搞一个叫做 zend_value 的 union(联合体),总体看来就是这样的:

好了,进入代码实战阶段,注意两点:

$a = 'hello'. mt_rand(1, 1000);

echo xdebug_debug_zval('a');

$b = $a;

echo xdebug_debug_zval('a');

$c = $a;

echo xdebug_debug_zval('a');

unset($c);

echo xdebug_debug_zval('a');

输出的结果是:

其中,zval struct 结构体用于保存 $a,zend_value union 联合体用于保存数据内容也就是'hello916'。由于后面又声明了 b 和 c,所以 C 不得不又在底层给你搞出两个 zval struct 结构体来。

其中,zval 和 zend value 的结构大概如下:(注意!!!这并不是完整正确的 PHP zval 和 zend_value 在 C 语言中 struct 和 union 实现,仅仅是挑出最重点的部分写出来,强调一下:你没有必要一个字不差背诵过 zval 和 zend_value,你只需要知道原理)

zval {

string "a" // 变量的名字是 a

value zend_value // 变量的值

type string // 变量是字符串类型

}

zend_value {

string "hello916" // 值的内容

refcount 1 // 引用计数

}

看到上面两个,如果面试官问你 php 变量为什么能够保存字符串 "123" 也能保存数字 123,你知道该怎么回答了吧?就答出重点 zval 中有该变量的类型,当是字符串 123 的时候,type 就是 string,此时 value 指向 “123”;当是整数 123 的时候,zval 的 type 为 int,value 为 123。这就是答题的思想,这很重要!而且,通过 C 语言都是可以实现的!具体真正的 val 和 zend_value 的模样,有兴趣的同学可以去网上搜搜,如果你没有 C 语言的底子,可能比较吃力!前者是一个 struct 结构体,后者是一个 union 联合体!

这个 refcount 就是传说中的引用计数了,初始化的时候 a 后面的引用次数为 1(注意,正确说法应该是 a 后面的赋值的数组 zend_value 引用计数为 1,而不是 a 这个变量 zval 本身)。然后我们将 $b = $a,其实相当于又一个变量指向了这个 zend_value,所以 refcount 变为 2,最后将 $c = $a,同理,zend_value 的 refcount 再次加 1 变成了 3。然后,我们用 unset($c),这会儿,C 语言要做的就是把 $c 的 zval 给 KO free 掉,但是并不是 free zend_value,这会儿 zend_value 的 refcount 就自然而然减 1 变成 2 了。

那么写时拷贝是什么意思呢?看下面代码:

<?php

// 先不要问为什么非要加 mt_rand,不然,绝笔说不过来了,到处都是坑

$a = 'hello'. mt_rand(1, 1000);

$b = $a;

$a = 123;

echo $b. PHP_EOL;

// 运行结果,不用我说吧,脚趾头都知道是'hello'.mt_rand(1, 1000) 的结果,绝对不可能是 123。

其实,当你把 $a 赋值给 $b 的时候,$a 的值并没有真的复制了一份,这样是对内存的极度不尊重,也是对时间复杂度的极度不尊重,计算机仅仅是将 $b 指向了 $a 的值而已,这就叫多快好省。那么,什么时候真正的发生复制呢?就是当我们修改 $a 的值为 123 的时候,这个时候就不得已进行复制,避免 $b 的值和 $a 的一样。

<?php

$a = 'hello'. mt_rand(1, 1000);

$b = $a;

echo xdebug_debug_zval('a');

$a = 'world'. mt_rand(2, 2000);

echo xdebug_debug_zval('a');

// 运行结果为 1,其中的原理你自己应该能理顺了昂

叨逼叨了这么长,通过简单的案例解释清楚了两个要点:引用计数和写时拷贝,那么垃圾回收也该来了。当一个 zval 在被 unset 的时候、或者从一个函数中运行完毕出来(就是局部变量)的时候等等很多地方,都会产生 zval 与 zend_value 发生断开的行为,这个时候 zend 引擎需要检测的就是 zend_value 的 refcount 是否为 0,如果为 0,则直接 KO free 空出内容来。如果 zend_value 的 recount 不为 0(废话一定是大于 0),这个 value 不能被释放,但是也不代表这个 zend_value 是清白的,因为此 zend_value 依然可能是个垃圾。

什么样的情况会导致 zend_value 的 refcount 不为 0,但是这个 zend_value 却是个垃圾呢?PHP7 种两种情况:

<?php

$arr = [1];

$arr[] = &$arr;

unset($arr);

这种情况下,zend_value 不会能释放,但也不能放过它,不然一定会产生内存泄漏,所以这会儿 zend_value 会被扔到一个叫做垃圾回收堆中,然后 zend 引擎会依次对垃圾回收堆中的这些 zend_value 进行二次检测,检测是不是由于上述两种情况造成的 refcount 为 1 但是自身却确实没有人再用了,如果一旦确定是上述两种情况造成的,那么就会将 zend_value 彻底抹掉释放内存。

那么垃圾回收发生在什么时候?有些同学可能有疑问,就是 php 不是运行一次就销毁了吗,我要着 gc 有何用?并不是啦,首先当一次 fpm 运行完毕后,最后一定还有 gc 的,这个销毁就是 gc;其次是,内存都是即用即释放的,而不是攒着非得到最后,你想想一个典型的场景,你的控制器里的某个方法里用了一个函数,函数需要一个巨大的数组参数,然后函数还需要修改这个巨大的数组参数,你们应该是函数的运行范围里面修改这个数组,所以此时会发生写时拷贝了,当函数运行完毕后,就得赶紧释放掉这块儿内存以供给其他进程使用,而不是非得等到本地 fpm request 彻底完成后才销毁。

说到最后,说些自己的话:大多数情况下,面试官问你问题主要是想一是要你个思维思路,二是看你学习程度。就像 gc 这个问题,其实很多脚本语言的垃圾回收机制基本上都是靠引用计数和写时拷贝这两种算法结合完成的,所以如果你设计一门脚本语言,gc 机制就按照这两种算法进行设计即可。其次是大多数 phper 不会看这些东西的,面试官问你这个问题不是要你死记硬背那么多细节,你背不过的,他还是想探测你平时有没有更积极地往深层发展的心态。

注重体现重点,很多细节实在没法写,比如我举个例子 $a=[],xdebug_debug_zval( $a) 的 refcount 值你猜是多少? 7.1.17 下竟然是 2,你是不是以为是 1,然而并不是。不过你不用纠结这些细节,gc 的关键就是能说出引用计数的原理和写时拷贝,很多细节深处都各种奇奇怪怪的东西,面试官自己都不一定知道。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章