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閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容