[ PHP 内核与扩展开发系列] 内存管理 —— 引用计数

对于 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 相关联,因为当复制发生时,内核还不知道第三个变量的名字。

原文地址:https://www.cnblogs.com/gongxianjin/p/10806643.html

时间: 2024-08-06 20:52:19

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

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

昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今天将会讲诉Linux如何追踪和管理用户空间进程的可用内存和内核的可用内存,还会讲到内核对内存分类的方式以及如何决定分配和释放内存,内存管理是应用程序通过软硬件协助来访问内存的一种方式,这里我们主要是介绍操作系统正常运行对内存的管理.插个话题,刚才和姐姐聊天,她快结婚了,说起了自己的初恋,可能是一句很搞笑的话,防火防盗防初恋,,嘎嘎,这个好像是的吧,尽管

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

Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 2016-11-24 09:36 114人阅读 评论(0) 收藏 举报  分类: PPAPI(27)  通过将浏览器所在客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机(native messaging host)",Chrome浏览器扩展还可以与客户端本地应用之间收发消息. 客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机"之后,Chrome浏览器会在独立的

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

对iOS开发中内存管理的一点总结与理解 做iOS开发也已经有两年的时间,觉得有必要沉下心去整理一些东西了,特别是一些基础的东西,虽然现在有ARC这种东西,但是我一直也没有去用过,个人觉得对内存操作的理解是衡量一个程序员成熟与否的一个标准.好了,闲话不说,下面进入正题. 众所周知,ObjectiveC的内存管理引用的一种叫做“引用计数“ (Reference Count)的操作方式,简单的理解就是系统为每一个创建出来的对象,(这里要注意,只是对象,NSObject的子类,基本类型没有‘引用计数’)

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

Chrome浏览器扩展开发系列之十四:本地消息机制Native messaging 时间:2015-10-08 16:17:59      阅读:1361      评论:0      收藏:0      [点我收藏+] 通过将浏览器所在客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机(native messaging host)",Chrome浏览器扩展还可以与客户端本地应用之间收发消息. 客户端的本地应用注册为Chrome浏览器扩展的"本地消息主机"

ARM裸机开发中内存管理库RT_HEAP的使用

在使用arm芯片进行裸机开发的时候,很多时候都需要内存管理的功能,我们可以使用自己写的内存管理程序,也可以直接使用标准库,不过我一般比较喜欢标准库,速度快,今天就来说说在C语言环境下怎么样进行内存的动态使用 首先,应该初始化C堆,初始化代码如下 #include "malloc.h" #pragma import (__use_realtime_heap) //这个函数在rt_heap.h中声明,需要用户自己去实现,返回任意值 unsigned __rt_heap_extend(uns

iOS开发ARC内存管理技术要点

本文来源于我个人的ARC学习笔记,旨在通过简明扼要的方式总结出iOS开发中ARC(Automatic Reference Counting,自动引用计数)内存管理技术的要点,所以不会涉及全部细节.这篇文章不是一篇标准的ARC使用教程,并假定读者已经对ARC有了一定了解和使用经验.详细的关于ARC的信息请参见苹果的官方文档与网上的其他教程:) 本文的主要内容: ARC的本质 ARC的开启与关闭 ARC的修饰符 ARC与Block ARC与Toll-Free Bridging 技术交流新QQ群:41

iOS开发_内存管理

1.ObjC中对象是存储在堆中的,系统并不会自动释放堆中的内存,但是基本类型是由系统自己管理的,放在栈上. 2.要开发一个程序并不难,但是优秀的程序则更测重于内存管理,它们往往占用内存更少,运行更加流畅. 3.野指针就是指针所指向的内存地址已经被系统回收,而指针没有被置为空;内存泄露就是该被回收的对象没有被回收,导致内存被占用,也就是内存泄露. 4.在ObjC中给空对象发送消息是不会引起错误的. 5.属性参数: retain:先release,再retain,用于非字符串对象 copy:先rel

iOS开发--漫谈内存管理(一)

1.MRC与ARC 苹果提供两种内存管理机制:一种是MRC(manual reference count),即手动引用计算:另一种是ARC(auto reference count),即自动引用计数.手动引用计,顾名思义,需要程序员主动调用retain.release等方法来管理内存,而自动引用计数方式,则是编译器在编译阶段把内存管理代码自动插入在需要手动调用的地方.这样就把程序员从复杂的管理内存的工作中解放出来,将更多精力放在实现具体的业务逻辑上.需要注意的是,ARC要在xcode4.2或者以

《Linux内核设计与实现》内存管理札记

1.页 芯作为物理页存储器管理的基本单元,MMU(内存管理单元)中的页表,从虚拟内存的角度来看,页就是最小单位. 内核用struct page结构来标识系统中的每个物理页.它的定义例如以下: flag域用来存放页的状态(是不是脏的.是不是被锁定在内存中等等)._count表示这一页被引用了多少次.当次数为0时,表示此页没有被引用,于是在新的分配中就能够使用它.virtual域是页的虚拟地址. 2.获得页 内核提供了一种请求内核的底层机制,并提供了对它进行訪问的几个接口. 全部这些接口都以页为单位