最近閱讀了《垃圾回收算法手冊》這本經典的書籍,藉此機會打算寫幾篇內存管理算法方面的文章,也算是本身的總結吧。 算法
—— 題記緩存
自動內存管理主要面臨如下三個方面的任務:性能
1.爲新對象分配內存空間優化
2.肯定「存活」對象atom
3.回收「死亡」對象所佔用的內存空間spa
其中任務1通常稱做「自動內存分配」(Memory Allocation ,下文簡稱 MA),任務二、3即是常說的「垃圾回收器」(Garbage Collect 即 gc ,下文簡稱GC)。通常來講,爲了下降自動內存管理系統設計的複雜性和穩定性,通常會採用單線程設計方式,也就是說MA和GC不能同時進行,這樣一來就解釋一些託管代碼在有大量GC的狀況下程序卡頓的狀況,由於在GC線程運行時,MA線程會等待,即託管代碼暫停執行,直到GC結束,若是此時GC運行時間較長,那麼程序卡頓的狀況就會較爲明顯。操作系統
基本的垃圾回收策略主要有四種:標記-清掃、標記-整理、複製回收法、引用計數,這裏把前兩種概括爲標記法。標記法回收策略分爲兩個過程:標記和回收,其中標記階段是指:從根集合開始用DFS搜索方式標記每一個存活對象(即對象可達:從根集合開始能夠找到該對象),回收是指:遍歷堆,把未標記的對象(即不可達對象)當作垃圾回收。不管採用何種回收策略,通常複製器的工做流程是相似的,這裏在先介紹一下賦值器的僞代碼:線程
New(): ref <—— allocate() //在堆中分配,可能會因內存碎片或者內存不足致使分配失敗 if ref = null collect() //執行回收策略 ref = allocate() if ref = null error "out of memory" return ref /* 全部標記回收策略均知足以下範式 先標記,後回收 其中atomic是原子操做關鍵字, 標識該方法以原子操做的方式執行完畢 */ atomic collect(): markFromRoots() sweep(HeapStart , HeapEnd)
標記-清掃是一種十分簡單的回收策略,其中的標記階段是標記法中通用的策略,這裏的「清掃」只是回收的最簡單實現。其主要思路:設計
1.從「根集合(Roots)」開始,DFS遍歷全部的對象,標記其是存活對象。【從Roots出發能夠訪問的對象可簡單看作是可達對象,近似等於存活對象】指針
2.線性遍歷堆,回收未標記的對象。【未標記即不可達,可看作非存活對象】
注:「Roots」是一個堆中的有限集合,複製器能夠直接訪問到,一般包括:靜態、全局存儲、線程本地存儲,簡單能夠當作「對象圖」中的入口對象集合
下面貼出標記-清掃最簡單的僞代碼:
//標記階段 markFromRoots(): initList(workList) //在DFS過程使用一個list做爲緩衝 for each fld in Roots //遍歷全部Roots對象,而後由根對象DFS到每一個可達對象,實現對存活對象的標記 ref <- *fld if ref ~= null setMarked(ref) //設置標記,這裏有不少種實現,如寫入對象頭部,位圖,字節圖等 add(workList) mark() initList(): workList <- empty //常規的DFS算法 mark(): while not isEmpty(workList) ref <- remove(workList) for each fld in Points(ref) child <- *fld if child != null && not isMarked(child) setMarked(child) add(workList) ============================================ //回收階段 sweep(start , end): scan <- start while scan < end if isMarked(scan) unsetMarked(scan) else free(scan) scan = nextObj(scan)
就像上面的僞代碼只是標記-清掃最簡單的描述,實際上它存在不少性能問題的,如時間局和空間局部性、沒法高效利用高速緩存、容易缺頁等,因此這裏只作最簡單的說明。
即便算法相對完備的「標記-清掃」回收策略也沒法避免「內存碎片」問題,由於該算法在「清掃」過程當中僅僅簡單地遍歷堆,直接釋放「不可達對象」,這樣一來必然形成內存碎片,瞭解操做系統的coder確定清楚,一旦內存碎片過可能是一件十分可怕的事情,極可能形成有內存卻沒法使用的狀況,最終致使內存崩掉……因此這時候就出現了「標記-整理」回收策略,它分爲「標記「和」「 整理」連個部分,其中標記部分和「標記-清掃」算法一致,區別在於回收策略上。
「標記-整理」法的整理過程有好幾種算法,大都傾向於將存活對象整理到堆的某一端,這裏介紹一種運用較爲普遍的算飯「List 2」算法,該算法主要分爲三部分:
1.計算整理後「存活對象」在堆中對應的地址
2.更新複製器的根及被標記對象的引用
3.真正移動對象到其對應的新地址
//「整理」算法的主體過程 Compact(): computeLocations(HeapStart , HeapEnd ,HeapStart) //計算整理後「存活對象」在堆中對應的地址 updateReferences(HeapStart ,HeapEnd) //更新引用 relocate(HeapStart , HeapEnd) //引動對象到最終位置 computeLocations(start , end , toRegion): scan <- start free <- toRegion while scan < end //從頭至尾掃描堆,找出被標記對象 if isMarked(scan) forwardingAddress(scan) <- free //forwardingAddress(scan) 表示該對象的「轉發地址」即整理後的地址, free <- free + size(scan) //可能在該對象頭信息中記錄,也可能以字節圖等形式記錄 scan <- scan + size(scan) updateReferences(start , end): for each fld in Roots //更新根引用地址 ref <- *fld if ref != null *fld <- forwardingAddress(ref) scan <- start while scan < end //掃描堆,更新其餘對象信息 if isMarked(scan) for each fld in Pointers(scan) if *fld != null *fld <- forwardingAddress(*fld) scan <- scan + size(scan) relocate(start , end): scan <- start while scan < end if isMarked(scan) dest <- forwardingAddress(scan) move(scan , dest) //真正移動對象 unsetMarked(dest) //去除記錄的轉發地址信息 scan <- scan + size;
相對於「標記-整理」策略須要屢次遍歷堆進行回收,「複製式回收」只須要遍歷一次堆,同時也清理了」內存碎片「,並保證了對象在堆中的相對順序(提升了程序的空間局部性)。可是它有個致命的缺點是堆的可利用空間只有一半。下面是算法的主要思想:
1.將堆空間平均分爲兩半(對象區和空閒區)
2.遍歷對象區,並把對象順序移到空閒區
3.遍歷結束,對象被整理到空閒區,釋放(即直接丟棄)原對象區
atomic collect(): flip() //分割半區 initList(workList) //做爲緩存棧 for each fld in Roots process(fld) //先處理根域 while not isEmpty(workList) //處理worklist中對象 ref <- remove(workList) scan(ref) //將堆平均分紅兩部分,假設HeapStart是存儲數據的堆 flip(): extent <- (HeapEnd - HeapStart) / 2 top <- HeapStart + extent free <- top //掃描給定對象的指針域 scan(ref): for each fld in Pointers(ref) process(fld) //更新對象的指針地址 process(fld): fromRef <- *fld if fromRef != null *fld <- forward(fromRef) //轉移對象 forward(): toRef <- forwardingAddress(fromRef) //forwardingAddress 記錄了對象的轉移地址 if toRef != null //判斷對象是否已經被轉移 toRef <- copy(fromRef) return toRef //將對象拷貝到堆的另外一個半區 copy(fromRef): toRef <- free free <- free + size(fromRef) //移動空閒指針 move(fromRef , toRef) forwardingAddress(fromRef) <- toRef //記錄轉移地址 add(workList , toRef) return toRef
基本的基於」標記「的內存管理策略主要就是上面兩種算法,其實優秀的內存管理策略確定不會僅僅只使用某種單一策略,它們可能更傾向於多種策略同時使用,好比在內存充足時可能就直接使用「複製式回收策略」,內存不足時切換成「標記-回收策略」,固然每種算法都會有各類優化策略,基本就是基於「空間和時間局部性」作優化,能夠很大程度提高回收器的效率,減小卡頓時間。