本文将要讲述 php 发展历程中的垃圾回收及内存管理相关内容。
引用计数
在 PHP 5.2 及以前的版本中,PHP 的垃圾回收采用的是 引用计数 算法。
引用计数基础知识
引用计数基础知识
php 的变量存储在「zval」变量容器(数据结构)中,「zval」属性包含如下信息:
当一个变量被赋值时,就会生成一个对应的「zavl」变量容器。【推荐学习:PHP视频教程】
查看变量 zval 容器信息
要查看变量的「zval」容器信息(即查看变量的 is_ref 和 refcount),可以使用 XDebug 调试工具的 xdebug_debug_zval() 函数。
安装 XDebug 扩展插件的方法可以查看 这个教程(),有关XDebug 使用方法请阅读 官方文档()。
假设,我们已经成功安装好 XDebug 工具,现在就可以来对变量进行调试了。
如果我们的 PHP 语句只是对变量进行简单赋值时,is_ref 标识值为 0,refcount 值为 1;若将这个变量作为值赋值给另一个变量时,则增加 zval 变量容器的 refcount 计数;同理,销毁(unset)变量时,「refcount」相应的减去 1。
请看下面的示例:
登录后复制
写时复制(Copy On Write:COW),简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时,才申请新空间来保存值内容以减少对内存的占用。 - TPIP 写时复制
通过前面的简单变量的 zval 信息我们知道 $copy 和 $name 共用 zval 变量容器(内存),然后通过 refcount 来表示当前这个 zval 被多少个变量使用。
看个实例:
登录后复制
注意到没有,当将值 liugongzi handsome 赋值给变量 $copy 时,name 和 copy 的 refcount 值都变成了 1,在这个过程中发生以下几个操作:
这里只是简单对「写时复制」进行介绍,感兴趣的朋友可以阅读文末给出的参考资料进行更加深入的研究。
引用传值(&)的「引用计数」规则同普通赋值语句一样,只是 is_ref 标识的值为 1 表示该变量是引用传值类型。
我们现在来看看引用传值的示例:
登录后复制
与标量类型(整型、浮点型、布尔型等)不同,数组(array)和对象(object)这种符合类型的引用计数规则会稍复杂一些。
为了更好的说明,还是先看看数组的引用计数示例:
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=2) // 'meaning' => (refcount=1, is_ref=0)string 'life' (length=4) // 'number' => (refcount=1, is_ref=0)int 42登录后复制
上面的引用计数示意图如下:
从图中我们发现复合类型的引用计数规则基本上同标量的计数规则一样,就给出的示例来说,PHP 会创建 3 个 zval 变量容器,一个用于存储数组本身,另外两个用于存储数组中的元素。
添加一个已经存在的元素到数组中时,它的引用计数器 refcount 会增加 1。
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); $a['life'] = $a['meaning']; xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=3) // 'meaning' => (refcount=2, is_ref=0)string 'life' (length=4) // 'number' => (refcount=0, is_ref=0)int 42 // 'life' => (refcount=2, is_ref=0)string 'life' (length=4)登录后复制
大致示意图如下:
虽然,复合类型的引用计数规则同标量类型大致相同,但是如果引用的值为变量自身(即循环应用),在处理不当时,就有可能会造成内存泄露的问题。
让我们来看看下面这个对数组进行引用传值的示例:
登录后复制
从内存占用结果上看,虽然我们执行了 unset($a) 方法来销毁 $a 数组,但内存并没有被回收,整个处理过程的示意图如下:
可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:
尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。 - 摘自 官方文档 Cleanup Problems
简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。
引用计数系统的同步周期回收
由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:
下图(来自 PHP 手册),展示了新的回收算法执行过程:
引用计数系统的同步周期回收过程缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);删除所有 refcount 为 0 的可能根(步骤 D)。
整个过程为:
采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。
优化后的引用计数算法优势
你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。
PHP 7 的内存管理
PHP 5 中 zval 实现上的主要问题:
PHP 7 中的 zval 数据结构实现的调整:
最基础的变化就是 zval 需要的内存 不再是单独从堆上分配,不再由 zval 存储引用计数。
复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。 - 摘自 Internal value representation in PHP 7 - Part 1【译】
这种实现的优势:
更具体的有关 PHP 7 zval 实现和内存优化细节可以阅读 深入理解 PHP7 内核之 zval 和 Internal value representation in PHP 7 - Part 1译。()
巧用margin/padding的百分比值实现高度自适应(多用于占位,避免闪烁)
一个基础却又容易混淆的css知识点
本文依赖于一个基础却又容易混淆的css知识点:当margin/padding取形式为百分比的值时,无论是left/right,还是top/bottom,都是以父元素的width为参照物的!
也许你会说,left/right以父元素的width为参照物好理解,但是top/bottom为什么也是以父元素的width为参照物的呢?网上众说纷纭,关键还是看W3C的规范:
Note that in a horizontal flow, percentages on ‘margin-top’ and ‘margin-bottom’ are relative to the width of the containing block, not the height (and in vertical flow, ‘margin-left’ and ‘margin-right’ are relative to the height, not the width).
Note that percentages on ‘padding-top’ and ‘padding-bottom’ are relative to the width of the containing block, not the height (at least in a horizontal flow; in a vertical flow they are relative to the height).
权威一出,记住就好,科科。
高度自适应占位
假设有这么个场景:
如上图所示,有这么一种用来放图片的容器,图片都是正方形(为了方便举例用正方形,实际上只要固定长宽比例即可)。
在PC端好办,容器的宽高都写死是多少px,这样即使图片加载不出来容器都不会变型。但是在移动端,由于各机型分辨率相差太大,写死px是绝对不可能的,终究还得靠百分比来实现自适应:
容器宽度设个50%吧,这样一行放俩容器,各占屏幕宽度一半,没问题。
图片宽度设个100%取容器的宽度,没问题。
容器高度没法设置啊,因为容器宽高的参照物不一样,而且需求是高度与宽度一致,所以无法通过为容器高度设置百分比来达成,那就只能靠内容高度撑开了。
容器的内容高度就是图片的高度,若图片是正方形,则图片高度与图片宽度一致,也即与容器宽度一致,看起来没问题是吧?实际上,在浏览器把图片加载出来以前,图片的高度是零,那可就没办法把容器撑开了,如下图所示:
这样一来,即使图片加载速度很快,容器在图片加载前后都会有一个变型的过程,也就是俗称的“闪烁”,而如果图片加载不出来,整体布局就更是难看了。
现在问题已经出来了,就是如何做到不靠图片本身就能把容器的高度撑开。
设置容器的padding-bottom/top
使用margin/padding的百分比值来解决自适应高度的关键在于:容器margin/padding的百分比参照物是父元素的宽度,而容器的width的百分比参照物也是父元素的宽度,俩属性参照物一致,那么想要把这俩属性的值统一起来就很简单了。
优化方案是这样的:给容器设置padding-top/padding-bottom跟width一致的值(百分比)。
#container {
width: 50%;//父元素宽度的一半
background-color: red;//仅为了方便演示
.placeholder{
padding-top: 50%;//与width: 50%;的值保持一致,也就是相当于父元素宽度的一半。
结果,容器的视觉效果如下:
容器的盒子模型如下:
从盒子模型可以看出,虽然容器的内容高度为0,但由于有了跟内容宽度一致的padding,因此整体视觉效果上像是被撑开了。此方案浏览器兼容性很不错,唯一的缺陷是无法给容器设置max-height属性了,因为max-height只能限制内容高度,而不能限制padding(我原以为设置box-sizing: border-box;可以让max-height限制padding,不过亲测无效,明白的朋友麻烦告知一下原因)。
给子元素/伪元素设置margin/padding撑开容器
从上面的方案看出max-height失效的原因是容器的高度本来就是padding撑的,而内容高度为0,max-height无法起作用。那想要优化这一点,唯一的方法就是利用内容高度来撑开而非padding,这个方案跟消除浮动所用的方案非常相似:给容器添加一个子元素/伪元素,并把子元素/伪元素的margin/padding设为100%,使其实际高度相当于容器的宽度,如此一来,便能把容器的高度撑至与宽度一致了。由于添加子元素与HTML语义化相悖,因此更推荐使用伪元素(:after)来实现此方案。
#container {
width: 50%;
position: relative;
background-color: red;
overflow: hidden;//需要触发BFC消除margin折叠的问题
.placeholder:after{
content: '';
display: block;
margin-top: 100%;//margin 百分比相对父元素宽度计算
此时视觉效果上与上一方案无异,重点来看看此时容器的盒子模型:
可以看出,此时容器的内容高度与内容宽度一致,妈妈再也不用担心我无法通过max-height来限制容器高度了。
另外,使用margin的话需要考虑margin折叠的问题(参考),padding则无此烦恼。
容器内部如何添加内容
上述方案只提及如何不依赖容器内容来撑开容器,那么,在撑开容器后,如何给容器添加内容(图片、文本等)呢?
答案很简单,那就是利用position: absolute;:
#container {
width: 50%;
position: relative;
background-color: red;
overflow: hidden;//需要触发BFC消除margin折叠的问题
.placeholder:after{
content: '';
display: block;
margin-top: 100%;//margin 百分比相对父元素宽度计算
img{
position: absolute;
top: 0;
width: 100%;
效果如下:
后补
宽高不一致的自适应怎么做?
有朋友可能会问,上面提到的都是宽度与高度一致的情况,如果不一致那怎么办呢?其实自适应的重点在于,元素的宽高必须维持一个固定的比例,比如说宽高一致比例就是1:1,宽是高的两倍那就是2:1,只要这个比例是明确而且固定的,那么只需要相应地修改margin/padding的百分比值即可适应不同的宽高比例。
还有其它的宽高自适应方案吗?
当然有,比如说css3新推出的长度单位vw,就是以屏幕宽度为参照物的,只要给元素的width和height都用上vw单位,那width跟height就可以轻易设成一样的了,不过既然是css3,浏览器兼容性肯定成问题:
总结
自适应的精髓在于宽度,margin/padding设置百分比弥补了元素高度无法自适应地与元素宽度保持一致的缺陷。
【今日微信公号推荐↓】