自動內存管理算法 —— 標記和複製法

最近閱讀了《垃圾回收算法手冊》這本經典的書籍,藉此機會打算寫幾篇內存管理算法方面的文章,也算是本身的總結吧。 算法

                                                                                                                                                                    —— 題記緩存

自動內存管理系統

    自動內存管理主要面臨如下三個方面的任務:性能

    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;
「標記-整理」回收策略較大的問題在於執行效率,由於大都須要屢次掃描堆,容易形成gc卡頓時間較長,再者相似list 2 這種用對象頭部記錄轉發地址信息的方式,也在必定程度上形成空間浪費。

三.複製式回收

    相對於「標記-整理」策略須要屢次遍歷堆進行回收,「複製式回收」只須要遍歷一次堆,同時也清理了」內存碎片「,並保證了對象在堆中的相對順序(提升了程序的空間局部性)。可是它有個致命的缺點是堆的可利用空間只有一半。下面是算法的主要思想:

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

 

總結

    基本的基於」標記「的內存管理策略主要就是上面兩種算法,其實優秀的內存管理策略確定不會僅僅只使用某種單一策略,它們可能更傾向於多種策略同時使用,好比在內存充足時可能就直接使用「複製式回收策略」,內存不足時切換成「標記-回收策略」,固然每種算法都會有各類優化策略,基本就是基於「空間和時間局部性」作優化,能夠很大程度提高回收器的效率,減小卡頓時間。

相關文章
相關標籤/搜索