对于 PHP 这种需要同时处理多个请求的程序来说,申请和释放内存的时候应该慎之又慎,一不小心便会酿成大错。另一方面,除了要安全申请和释放内存外,还应该做到内存的最小化使用,因为它可能要处理每秒钟数以千计的请求,为了提高系统整体的性能,每一次操作都应该只使用最少的内存,对于不必要的相同数据的复制则应该能免则免。我们来看下面这段 PHP 代码:

<?php
$a = 'Hello World';
$b = $a;
unset($a);

第一条语句执行后,PHP 创建了 $a 这个变量,并为它申请了 12B 的内存来存放"Hello World"这个字符串(最后加个NULL字符,你懂的),紧接着把 $a 赋给了 $b,并释放掉 $a

对于 PHP 来说,如果每一次变量赋值都执行一次内存复制的话,那需要额外申请 12B 的内存来存放这个重复的数据,当然为了复制内存,还需要 CPU 执行某些计算,这当然会加重 CPU 的负载。当第三句执行后,$a被释放了,我们刚才的设想突然变的这么滑稽,这次赋值显得好多余哦。如果早就知道 $a 不用了,那我们直接让 $b 用 $a 的内存不就行了,还赋值干嘛?如果你觉得 12B 没什么,那设想下如果 $a 是个10M的文件内容,或者20M,是不是我们的计算机资源消耗的有点冤枉呢?

别担心,PHP 很聪明!前面章节说过,PHP 变量的名称和值在内核中是保存在两个不同的地方的,值是通过一个与名字毫无关系的 zval 结构来保存,而这个变量的名字 a 则保存在符号表里,两者之间通过指针联系着。在我们上面的例子里,$a 是一个字符串,我们通过 zend_hash_add 把它添加到符号表里,然后又把它赋值给 $b,两者拥有相同的内容。如果两者指向完全相同的内容,我们有什么优化措施吗?

zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), &helloval, sizeof(zval*), NULL);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), &helloval, sizeof(zval*), NULL);
//通过这个例子我们看出了,我们可以把$a和$b都指向helloval~!

现在我们检查 $a 和 $b 两个变量,他们的值指向了 "Hello World" 这个字符串在内存中的位置。但是在第三行:unset($a); 这条语句释放了 $a。在这种情况下,unset 函数并不知道$a的值同时被 $b 用着,所以如果它直接释放内存,则会导致 $b 的值也被清空了,从而导致逻辑错误,甚至可能会导致系统崩溃。

呵呵,其实你心里明白,PHP 不会让上述问题发生的!回顾一下 zval 的四个成员 valuetypeis_ref__gcrefcount__gc,我们对 value 和 type 已经很熟了,现在则是后两个成员发挥威力的时候了,这里我们主要讲解 refcount__gc 这个成员。当一个变量被第一次创建的时候,它对应的zval 结构体的 refcount__gc 成员的值会被初始化为1,理由很简单,因为只有这个变量自己在用它。但是当你把这个变量赋值给别的变量时,refcount__gc 属性便会加1变成2,因为现在有两个变量在用这个zval 结构了!以上描述转为内核中的代码大体如下:

zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), &helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(helloval); //这句很特殊,我们显式的增加了helloval结构体的refcount
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), &helloval, sizeof(zval*), NULL);

这个时候当我们再用 unset 删除 $a 的时候,它删除符号表里的 $a 的信息,然后清理它的值部分,这时它发现 $a的值对应的 zval 结构的 refcount 值是2,也就是有另外一个变量在一起用着这个 zval,所以 unset 只需把这个 zval 的 refcount 减去1就行了!

写时复制

引用计数绝对是节省内存的一个超棒的模式!但是当我们修改 $b 的值,而且还需要继续使用 $a 时,该怎么办呢?

$a = 1;
$b = $a;
$b += 5;

从代码逻辑来看,我们希望语句执行后 $a 仍然是1,而 $b 则需要变成6。我们知道在第二句完成后内核通过让 $a和 $b 共享一个 zval 结构来达到节省内存的目的,但是现在第三句来了,这时 $b 的改变应该怎样在内核中实现呢? 答案非常简单,内核首先查看 refcount__gc 属性,如果它大于1则为这个变化的变量从原 zval 结构中复制出一份新的专属于 $b 的zval来,并改变其值。

zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
zval **varval, *varcopy;
if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE)
{
/* 如果在符号表里找不到这个变量则直接return */
return NULL;
} if ((*varval)->refcount < 2)
{
//如果这个变量的zval部分的refcount小于2,代表没有别的变量在用,return
return *varval;
} /* 否则,复制一份zval*的值 */
MAKE_STD_ZVAL(varcopy);
varcopy = *varval; /* 复制任何在zval*内已分配的结构*/
zval_copy_ctor(varcopy); /* 从符号表中删除原来的变量
* 这将减少该过程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1); /* 初始化新的zval的refcount,并在符号表中重新添加此变量信息,并将其值与我们的新zval相关联。*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL); /* 返回新zval的地址 */
return varcopy;
}

现在 $b 变量拥有了自己的 zval,并且可以自由的修改它的值了。

写时修改

如果用户在 PHP 脚本中显式的让一个变量引用另一个变量时,我们的内核是如何处理的呢?

$a = 1;
$b = &$a;
$b += 5;

作为一个标准的 PHP 程序员,我们都知道 $a 的值也变成6了。当我们更改 $b 的值时,内核发现 $b 是 $a 的一个用户端引用,也就是所它可以直接改变 $b 对应的 zval 的值,而无需再为它生成一个新的不同于 $a 的zval。因为他知道 $a 和 $b 都想得到这次变化!

但是内核是怎么知道这一切的呢?简单的讲,它是通过 zval 的 is_ref__gc 成员来获取这些信息的。这个成员只有两个值,就像开关的开与关一样。它的这两个状态代表着它是否是一个用户在 PHP 语言中定义的引用。在第一条语句 $a = 1; 执行完毕后,$a对应的 zval 的 refcount__gc 等于1,is_ref__gc 等于0。当第二条语句执行后$b = &$a;refcount__gc 属性向往常一样增长为2,而且 is_ref__gc 属性也同时变为了1!最后,在执行第三条语句的时候,内核再次检查 $b 的 zval以确定是否需要复制出一份新的 zval 结构来,这次不需要复制,因为我们刚才上面的get_var_and_separate 函数其实是个简化版,并且少写了一个条件:

/* 如果这个zval在PHP语言中是通过引用的形式存在,或者它的refcount小于2,则不需要复制。*/
if ((*varval)->is_ref || (*varval)->refcount < 2) {
return *varval;
}

这一次,尽管它的 refcount 等于2,但是因为它的 is_ref 等于1,所以也不会被复制。内核会直接的修改这个 zval 的值。

合理分离

我们已经了解了 PHP 语言中变量的复制和引用的一些事,但是如果复制和引用这两个事件被组合起来使用了该怎么办呢?看下面这段代码:

$a = 1;
$b = $a;
$c = &$a;

这里我们可以看到$a$b$c这三个变量现在共用一个 zval 结构,有两个属于 change-on-write组合($a$c),有两个属于copy-on-write组合($a$b),我们的 is_ref__gc 和refcount__gc 该怎样工作,才能正确的处理好这段复杂的关系呢?答案是: 不可能!在这种情况下,变量的值必须分离成两份完全独立的存在!$a 与 $c 共用一个zval,$b 自己用一个zval,尽管他们拥有同样的值,但是必须至少通过两个 zval 来实现。见下图(在引用时强制复制):

同样,下面的这段代码同样会在内核中产生歧义,所以需要强制复制!

//上图对应的代码
$a = 1;
$b = &$a;
$c = $a;

需要注意的是,在这两种情况下,$b 都与原初的 zval 相关联,因为当复制发生时,内核还不知道第三个变量的名字。

[ PHP 内核与扩展开发系列] 内存管理 —— 引用计数的更多相关文章

  1. OC开发系列-内存管理

    概述 移动设备的内存极其有限,每个app所有占用的内存是有限的.当app所占用的内存比较多时,系统会发出内存警告,这时得回收一些不需要再使用的内存空间. 任何集成了NSObject的对象都需要手动进行 ...

  2. python9--内存管理 引用计数 标记清除 分代回收

     复习   文件处理 1.操作文件的三步骤 -- 打开文件:硬盘的空间被操作系统持有 | 文件对象被应用程序持续 -- 操作文件:读写操作 -- 释放文件:释放操作系统对硬盘空间的持有 2.基础的读写 ...

  3. 十天学Linux内核之第三天---内存管理方式

    原文:十天学Linux内核之第三天---内存管理方式 昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今 ...

  4. 高性能JAVA开发之内存管理

    这几天在找一个程序的bug,主要是java虚拟机内存溢出的问题,调研了一些java内存管理的资料,现整理如下: 一.JVM中的对象生命周期 对象的生命周期一般分为7个阶段:创建阶段,应用阶段,不可视阶 ...

  5. Chrome浏览器扩展开发系列之十四

    Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 时间:2015-10-08 16:17:59      阅读:1361      评论:0      收藏:0    ...

  6. IOS开发的内存管理

    关于IOS开发的内存管理的文章已经很多了,因此系统的知识点就不写了,这里我写点平时工作遇到的疑问以及解答做个总结吧,相信也会有人遇到相同的疑问呢,欢迎学习IOS的朋友请加ios技术交流群:190956 ...

  7. IOS开发小记-内存管理

    关于IOS开发的内存管理的文章已经很多了,因此系统的知识点就不写了,这里我写点平时工作遇到的疑问以及解答做个总结吧,相信也会有人遇到相同的疑问呢,欢迎学习IOS的朋友请加ios技术交流群:190956 ...

  8. [转载]对iOS开发中内存管理的一点总结与理解

    对iOS开发中内存管理的一点总结与理解   做iOS开发也已经有两年的时间,觉得有必要沉下心去整理一些东西了,特别是一些基础的东西,虽然现在有ARC这种东西,但是我一直也没有去用过,个人觉得对内存操作 ...

  9. Chrome浏览器扩展开发系列之十四:本地消息机制Native messagin

    Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 2016-11-24 09:36 114人阅读 评论(0) 收藏 举报  分类: PPAPI(27)  通过将浏览器 ...

  10. Code First开发系列之管理数据库创建,填充种子数据以及LINQ操作详解

    返回<8天掌握EF的Code First开发>总目录 本篇目录 管理数据库创建 管理数据库连接 管理数据库初始化 填充种子数据 LINQ to Entities详解 什么是LINQ to ...

随机推荐

  1. Vmware虚拟主机访问外网设置

    本手册使用10.4.7.0/24网段 重点在于虚拟主机的网关和宿主机上的Vmnet8的IP和虚拟网络编辑器的NET网关保持一致 1.设置宿主机网络适配器 选择允许Vmware网络共享 配置VMnet8 ...

  2. PostgreSQL 欺骗优化器之扩展统计信息

    一.什么是扩展统计 扩展统计对象, 追踪指定表.外部表或物化视图的数据. 目前支持的种类: 启用n-distinct统计的 ndistinct. 启用功能依赖性统计的dependencies. 启用最 ...

  3. 《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(18)-Fiddler如何接口测试,妈妈再也不担心我不会接口测试了

    1.简介 Fiddler最大的优势在于抓包,我们大部分使用的功能也在抓包的功能上,fiddler做接口测试也是非常方便的. 领导或者开发给你安排接口测试的工作任务,但是没有给你接口文档(由于开发周期没 ...

  4. Windows Server体验之升级安装

    由于Windows Server的版本是每半年更新的,而微软对于产品的支持周期又是有限的.比如Windows Server 1803的主流支持只到2019年11月12日.也就是说,最多1年半,就需要更 ...

  5. 前端实现docx、pdf格式文件在线预览

    theme: vuepress highlight: atelier-heath-light 介绍 在业务中,如果遇到文档管理类的功能,会出现需要在线预览的业务需求,本文主要是通过第三方库来实现文档预 ...

  6. 【学习笔记】Vins-Mono论文阅读笔记(二)

    估计器初始化简述 单目紧耦合VIO是一个高度非线性的系统,需要在一开始就进行准确的初始化估计.通过将IMU预积分与纯视觉结构进行松耦合对齐,我们得到了必要的初始值. 理解:这里初始化是指通过之前imu ...

  7. 使用ConfigMap配置您的应用程序

    转载自:https://kuboard.cn/learning/k8s-intermediate/config/config-map.html ConfigMap 作为 Kubernetes API ...

  8. loam详细代码解析与公式推导

    loam详细代码解析与公式推导(基础理论知识) 一.基础坐标变换 loam中欧拉角解算都采用R P Y 的解算方式,即先左乘R, 再左乘P, 最后左乘Y,用矩阵表示为: R = Ry * Rp * R ...

  9. vue基础之MV*和它们之间的不同

    vue中的设计思想 vue中的设计思想主要是MV*模式,由最早的MVC(model-view-controller)框架,到后面的MVP(model-view-presenter),甚至到最后的MVV ...

  10. PAT (Basic Level) Practice 1030 完美数列 分数 25

    给定一个正整数数列,和正整数 p,设这个数列中的最大值是 M,最小值是 m,如果 M≤mp,则称这个数列是完美数列. 现在给定参数 p 和一些正整数,请你从中选择尽可能多的数构成一个完美数列. 输入格 ...