PHP内存管理 垃圾回收

本文是php-internals的读书笔记.

概述
1) 操作系统直接管理着内存,所以操作系统也需要进行内存管理,计算机中通常都有内存管理单元(MMU) 用于处理CPU对内存的访问。
2) 应用程序无法直接调用物理内存, 只能向系统申请内存。
向操作系统申请内存空间会引发系统调用。
系统调用会将CPU从用户态切换到内核。
为了减少系统调用开销。通常在用户态进行内存管理。 申请大块内存备用。使用完的内存不马上释放,将内存复用,避免多次内存申请和释放所带来性能消耗。
3) PHP不需要显示内存管理,由Zend引擎进行管理。
PHP内存限制
1)php.ini中的默认32MB
memory_limit = 32M
2)动态修改内存
ini_set ("memory_limit", "128M")

3)获取目前内存占用
memory_get_usage() : 获取PHP脚本所用的内存大小
memory_get_peak_usage() :返回当前脚本到目前位置所占用的内存峰值。

学习内存管理的目的
了解PHP如何占用内存,可以避免不必要的内存浪费。

PHP中的内存管理###

包含:
1)足够内存
2)可用内存获取部分内存
3)使用后的内存,是否销毁还是重新分配

PHP内存管理器

clipboard.png

接口层,是一些宏定义。
**堆层 heap **
_zend_mm_heap

初始化内存,调用 zend_mm_startup
PHP内存管理维护三个列表:
1)小块内存列表 free_buckets
2)大块内存列表 large_free_buckets
3)剩余内存列表 rest_buckets

两个HashTable 结构,难点是查找和计算内存地址
1)free_buckets
Hash函数为:

#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))

2)large_free_buckets
Hash函数为:

#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S)

    static inline unsigned int zend_mm_high_bit(size_t _size){
       ..//省略若干不同环境的实现
      unsignedint n =0;
      while(_size !=0) { 
        _size = _size >>1; n++;}
        return n-1;
    }

存储层 storage

  • 内存分配的方式对堆层透明化,实现存储层和heap层的分离。
  • 不同的内存分配方案, 有对应的处理函数。

内存的申请

PHP底层对内存的管理, 围绕着小块内存列表(free_buckets)、 大块内存列表(large_free_buckets)和 剩余内存列表(rest_buckets)三个列表来分层进行的

ZendMM向系统进行的内存申请,并不是有需要时向系统即时申请, 而是由ZendMM的最底层(heap层)先向系统申请一大块的内存,通过对上面三种列表的填充, 建立一个类似于内存池的管理机制。 在程序运行需要使用内存的时候,ZendMM会在内存池中分配相应的内存供使用。 这样做的好处是避免了PHP向系统频繁的内存申请操作

ZendMM对内存分配的处理步骤:

1)内存检查;
2)命中缓存,找到内存块,调至步骤5;
3)在ZendMM管理的heap层存储中搜索合适大小的内存块, 是在三种列表中小到大进行的,找到block后,调至步骤5;
4)步骤3未找到内存,则使用 ZEND_MM_STORAGE_ALLOC 申请新内存块 (至少为ZEND_MM_SEG_SIZE),进行步骤6

5)使用zend_mm_remove_from_free_list函数将已经使用block节点在zend_mm_free_block中移除;
6) 内存分配完毕,对zend_mm_heap结构中的各种标识型变量进行维护,包括large_free_buckets, peak,size等;
7) 返回分配的内存地址;

PHP内存管理器

内存的销毁

ZendMM在内存销毁的处理上采用与内存申请相同的策略,当程序unset一个变量或者是其他的释放行为时, ZendMM并不会直接立刻将内存交回给系统,而是只在自身维护的内存池中将其重新标识为可用, 按照内存的大小整理到上面所说的三种列表(small,large,free)之中,以备下次内存申请时使用。

ZendMM将内存块以整理收回到zend_mm_heap的方式,回收到内存池中。
程序使用的所有内存,将在进程结束时统一交还给系统。

垃圾回收

自动回收内存的过程叫垃圾收集。PHP提供了语言层的垃圾回收机制,让程序员不必过分关心程序内存分配。

PHP5.3之前
引用计数方式的内存动态管理。

PHP中所有的变量都是以zval变量的形式存在。

变量引用计数变为0时,PHP将在内存中销毁这个变量。只是这里的垃圾并不能称之为垃圾。并且PHP在一个生命周期结束后就会释放此进程/线程所占的内容,这种方式决定了PHP在前期不需要过多考虑内存的泄露问题。

PHP5.3的垃圾回收

引入垃圾收集机制的目的是为了打破引用计数中的循环引用,从而防止因为这个而产生的内存泄露。 垃圾收集机制基于PHP的动态内存管理而存在。PHP5.3为引入垃圾收集机制,在变量存储的基本结构上有一些变动.

struct _zval_struct {
  /* Variable information */ 
  zvalue_value value;/* value */ 
  zend_uint refcount__gc; 
  zend_uchar type;/* active type */ 
  zend_uchar is_ref__gc;
};

添加了 __gc 以用于新的垃圾回收机制。

PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

PHP5.3的垃圾回收算法仍然以引用计数为基础,但是不再是使用简单计数作为回收准则,而是使用了一种同步回收算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出。
论文较复杂, 列出一些大体描述。
首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。
由上文我们可以知道,一个zval如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root)。这里我们暂且不讨论PHP是如何发现这些可能根的,这是个很复杂的问题,总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。
当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:
1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。
2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。
3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。
如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:
1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。
2、可以解决循环引用问题。
3、可以总将内存泄露保持在一个阈值以下。

PHP5.2与PHP5.3垃圾回收算法的性能比较

PHP Manual中的相关章节:http://docs.php.net/manual/zh/features.gc.performance-considerations.php

首先是内存泄露试验,下面直接引用PHP Manual中的实验代码和试验结果图:


<?php

class Foo
{
   public $var = '3.1415962654';
}

$baseMemory = memory_get_usage();

for ( $i = 0; $i <= 100000; $i++ )
{
   $a = new Foo;
   $a->self = $a;
   if ( $i % 500 === 0 )
   {
       echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
   }
}

?>

gc-benchmark.png

可以看到在可能引发累积性内存泄露的场景下,PHP5.2发生持续累积性内存泄露,而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。

与垃圾回收算法相关的PHP配置

1、可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP的垃圾回收机制,也可以通过调用gc_enable()或gc_disable()打开或关闭PHP的垃圾回收机制。
2、在PHP5.3中即使关闭了垃圾回收机制,PHP仍然会记录可能根到根缓冲区,只是当根缓冲区满额时,PHP不会自动运行垃圾回收
3、当然,任何时候您都可以通过手工调用gc_collect_cycles()函数强制执行内存回收。


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

推荐阅读更多精彩内容