動態記憶體管理

chszs發表於2009-02-19
版權宣告:本文為博主chszs的原創文章,未經博主允許不得轉載。 https://blog.csdn.net/chszs/article/details/3908401

動態記憶體管理

動態記憶體管理DMM(Dynamic Memory Management)是從Heap中直接分配記憶體和回收記憶體。

有兩種方法實現動態記憶體管理。

一是顯示記憶體管理EMM(Explicit Memory Management)。
在EMM方式,記憶體從Heap中進行分配,用完後手動回收。程式使用malloc()函式分配整數陣列,並使用free()函式釋放分配的記憶體。

二是自動記憶體管理AMM(Automatic Memory Management)。
AMM也可叫垃圾回收器(Garbage Collection)。Java程式語言實現了AMM,與EMM不同,Run-time system關注已分配的記憶體空間,一旦不再使用,立即回收。

無論是EMM還是AMM,所有的Heap管理計劃都面臨一些共同的問題和前在的缺陷:
1)內部碎片(Internal Fragmentation)
當記憶體有浪費時,內部碎片出現。因為記憶體請求可導致分配的記憶體塊過大。比如請求128位元組的儲存空間,結果Run-time system分配了512位元組。

2)外部碎片(External Fragmentation)
當一系列的記憶體請求留下了數個有效的記憶體塊,但這些記憶體塊的大小均不能滿足新請求服務,此時出現外部碎片。

3)基於定位的延遲(Location-based Latency)
延遲問題出現在兩個資料值儲存得相隔很遠,導致訪問時間增加。

EMM往往比AMM更快。
EMM與AMM比較表:
——————————————————————————————————————
                                 EMM                                        AMM
——————————————————————————————————————
Benefits     尺寸更小、速度更快、易控制         stay focused on domain issues
Costs        複雜、記賬、記憶體洩露、指標懸空           不錯的效能
——————————————————————————————————————

早期的垃圾回收器非常慢,往往佔用50%的執行時間。

垃圾回收器理論產生於1959年,Dan Edwards在Lisp程式語言的開發時實現了第一個垃圾回收器。

垃圾回收器有三種基本的經典演算法:

1)Reference counting(引用計數)
基本思想是:當物件建立並賦值時該物件的引用計數器置1,每當物件給任意變數賦值時,引用記數+1;一旦退出作用域則引用記數-1。一旦引用記數變為0,則該物件可以被垃圾回收。
引用記數有其相應的優勢:對程式的執行來說,每次操作只需要花費很小塊的時間。這對於不能被過長中斷的實時系統來說有著天然的優勢。
但也有其不足:不能夠檢測到環(兩個物件的互相引用);同時在每次增加或者減少引用記數的時候比較費時間。
在現代的垃圾回收演算法中,引用記數已經不再使用。

2)Mark-sweep(標記清理)
基本思想是:每次從根集出發尋找所有的引用(稱為活物件),每找到一個,則對其做出標記,當追蹤完成之後,所有的未標記物件便是需要回收的垃圾。
也叫追蹤演算法,基於標記並清除。這個垃圾回收步驟分為兩個階段:在標記階段,垃圾回收器遍歷整棵引用樹並標記每一個遇到的物件。在清除階段,未標記的物件被釋放,並使其在記憶體中可用。

3)Copying collection(複製收集)
基本思想是:將記憶體劃分為兩塊,一塊是當前正在使用;另一塊是當前未用。每次分配時使用當前正在使用記憶體,當無可用記憶體時,對該區域記憶體進行標記,並將標記的物件全部拷貝到當前未用記憶體區,這是反轉兩區域,即當前可用區域變為當前未用,而當前未用變為當前可用,繼續執行該演算法。
拷貝演算法需要停止所有的程式活動,然後開始冗長而繁忙的copy工作。這點是其不利的地方。

近年來還有兩種演算法:

1)Generational garbage collection(分代)
其思想依據是:
  (1) 被大多數程式建立的大多數物件有著非常短的生存期。
  (2) 被大多數程式建立的部分物件有著非常長的生存期。
簡單拷貝演算法的主要不足是它們花費了更多的時間去拷貝了一些長期生存的物件。
而分代演算法的基本思想是:將記憶體區域分兩塊(或更多),其中一塊代表年輕代,另一塊代表老的一代。針對不同的特點,對年輕一代的垃圾收集更為頻繁,對老代的收集則較少,每次經過年輕一代的垃圾回收總會有未被收集的活物件,這些活物件經過收集之後會增加成熟度,當成熟度到達一定程度,則將其放進老代記憶體塊中。
分代演算法很好的實現了垃圾回收的動態性,同時避免了記憶體碎片,是目前許多JVM使用的垃圾回收演算法。

2)Conservative garbage collection(保守)

哪一種演算法最好?答案是沒有最好。

EMM作為很常用的垃圾回收演算法,有5種基本方法:
1)Table-driven algorithms
表驅動演算法把記憶體分為固定尺寸的塊集合。這些塊使用抽象資料結構進行索引。比如一個bit對應一個塊,用0和1表示是否分配。不利因素:位對映依賴於記憶體塊的尺寸;另外,搜尋一系列的空閒記憶體塊可能需要搜尋整個bit對映表,這影響效能。

2)Sequential fit
順序適應演算法允許記憶體分為不同的尺寸。此演算法跟蹤已分配和空閒的Heap,標記空閒塊的起始地址和結束地址。它有三種子分類:
  (1) First fit(首次適應)——分配找到的第一個適合記憶體請求的塊
  (2) Best fit(最佳適應)——分配最適合記憶體請求的塊
  (3) Worst fit(最不適應)——分配最大的塊給記憶體請求

3)Buddy systems
Buddy systems演算法的主要目的是加速已分配記憶體在釋放後的合併速度。顯示記憶體管理EMM使用Buddy systems演算法可能導致內部碎片。

4)Segregated storage
隔離儲存技術涉及到把Heap分成多個區域(zone),併為每個區域採用不同的記憶體管理計劃。這是很有效的方法。

5)Sub-allocators
子配置技術嘗試解決在Run-time System下分配大塊記憶體並單獨管理的記憶體分配問題。換句話說,程式完全負責自己的私有儲存堆(stockpile)的記憶體分配和回收,無需run-time System的幫助。它可能帶來額外的複雜性,但是你可以顯著地提高效能。在1990年的《C Compiler Design》一書中,Allen Holub就極好地利用了Sub-allocators來加速其編譯器的實現。

注意,顯示記憶體管理EMM必須是靈活的,能夠響應數種不同型別的請求。

最後,使用EMM還是使用AMM?這是一個Religious question,憑個人喜好。EMM在複雜的開銷下實現了速度和控制。AMM犧牲了效能,但換來了簡單性。


相關文章