聊聊垃圾回收 GC

GC 全稱 Garbage Collection ,即:垃圾回收。javascript

計算機程序垃圾指的是什麼呢?

寫過計算機程序的都知道,程序中無時無刻伴不隨着變量的引用,賦值,運算等操做。因而乎存在着某些變量在使用事後,程序不會再用到它們,可是他們依然佔據着必定的內存空間。內存中這樣不會被程序再次使用的數據即可稱之爲‘垃圾’。java

什麼是回收?

簡而言之,回收就是將上面講到的 程序運行時產生的‘垃圾’ 釋放掉,以便於程序再次使用這塊內存區域。算法

GC 的歷史

  • 1960 年 John McCarthy  首次發佈著名的CG 算法 ,GC 標記-清除算法
  • 1960 年 George E. Collins  發佈了引用計數的GC算法
  • 1963 年 Marvin L. Minsky 發佈了 GC 賦值算法

使人驚歎的是,目前全部的GC算法只不過是上述3種算法的組合或應用,也就是說從1963年賦值算法誕生時,到目前爲止沒有誕生新的GC算法!瀏覽器

GC 中的基本概念

首先咱們明確個知識點,GC在內存中銷燬移動的基本單位是對象。
bash

對象

對象由頭(head)和 域(field)構成。
函數

對象.png

頭 head

對象中保存對象自己信息的部分稱之爲‘頭’。head中的信息用戶沒法訪問和修改,包含下列信息。優化

  • 對象的大小
  • 對象種類

域 field

field 對於咱們而言比較熟悉,在JavaScript使用屬性操做符即可以直接訪問的部分被稱之爲域。域包含兩種數據類型。ui

  • 指針 ,   即:引用數據類型
  • 非指針, 即:基本數據類型,例如 true , false , 1, ……。

mutator

指的是修改CG對象之間的引用關係。更準確的來說它就是 ‘應用程序’自己,也就是咱們寫的代碼。
this

堆 heap

堆指的是用於動態(也就是執行程序時)存放對象的內存空間。當 mutator 申請存放對象時, 所需的內存空間就會從這個堆中被分配給 mutator。spa

活動對象/非活動對像

也就是分配到內存空間中的對象中那些可以經過 mutator 引用的對象稱之爲 ‘活動對象’,反之爲‘非活動對象’即:垃圾。

根 root

在CG世界中,根指的是對象指針的起點部分。也就是**mutator **(應用程序) 中的全局變量,經過遞歸這些根(全局變量)能夠遍歷到的對象就是活動對象,反之就是非活動對象(垃圾)。

image.png

(根與堆中對象的關係)

GC標記-清除算法

如該算法的名稱所描述,它的執行能夠分爲兩個階段,即:標記清除標記就是經過上面提到的根(全局對象)能遍歷到的內存中的對象標記爲活動對象,標記完成以後接下來就清除內存中沒有被標記的對象,也就是非活動對象。經過這兩個階節階段實現了內存空間的重複利用

標記階段

標記過程實際上能夠經過僞代碼來更加具體展示:

/*標記函數*/
mark_phase(){  
  // 遍歷全局對象,即:根
  for(r : $roots)    
    	mark(*r) 
}

/* 標記遍歷到的對象,而後繼續遍歷該對象的引用對象。 標記過程採用的是深度優先算法 */
mark(obj){
    // 遍歷到已經設置爲 true 的對象則再也不處理
    if(obj.mark == FALSE)
        obj.mark = TRUE
    // 標記後繼續遍歷當前對象的引用對象,直至全部的活動對象都被遍歷到
    for(child : children(obj))      
    	mark(*child) 
}
複製代碼

清除階段

清除階段的遍歷過程是從堆的首地址開始,一個個的遍歷對象的標誌位。

僞代碼以下所示:

sweep_phase(){
    //  $heap_start 指針指堆中的第一個對象的指針
    sweeping = $heap_start
    // 遍歷堆時的邊界控制,不能超出堆的大小
    while(sweeping < $heap_end)
    if(sweeping.mark == TRUE)
    	// 遇到標記爲 true 的對象,則取消標誌位,準備下一次GC
      sweeping.mark = FALSE
    else
    	// 這裏就是關鍵的回收處理了,首先$free_list 就是‘空閒鏈表’,程序須要的內存就是從其中分配得到。
        // 可能有小夥伴看不懂這裏,就詳細解釋一下:
        /*
      	     $free_list 是指向‘空閒鏈表’的指針,將它賦值給要回收對象的 next 域,那麼接下來這個要被回收
             的對象就變成了‘空閒鏈表’的頭,即:header,也就是這個對象被加入到了空閒鏈表中,接下來就會被
            當作空閒空間來分配給應用程序。最後再將頭指針賦給 $free_list 變量,也就是 $free_list 回指向
            空閒鏈表,而後繼續遍歷……
        */
        sweeping.next = $free_list
        $free_list = sweeping
    
    // size 保存着被回收對象的大小,這步操做實際上就是移動至堆中的下一個對象,進行回收操做。   
    sweeping += sweeping.size
}

複製代碼

經過下圖能夠形象的看出完成一次 GC 回收事後堆的狀態:

image.png

(一次GC回收事後堆的狀態)


引用計數法

GC 被本質上來講就是一種研究如何釋放沒法被引用對象的技術。那麼,能夠想到,若是讓對象本身記錄一下,它有沒有被程序引用。這就是引用計數法

image.png

(引用計數法的對象)


引用計數法與 **mutator(應用程序)**的執行密切相關,也就是在程序處理數據對象的過程當中經過增減計數器來實現內存管理。 在對象的生成和被引用時會發生計數器的增減,也就是  new_obj() 函數和 update_ptr() 函數。

new_obj() 函數,分配內存

僞代碼以下:

new_obj(size){
    // 從‘空閒鏈表’中爲程序分配空間
    obj = pickup_chunk(size, $free_list)
  // 分配空間失敗
  if(obj == NULL)    
    allocation_fail()  
  else
    // 爲分配成功的對象設置計數器,並初始化
    obj.ref_cnt = 1    
    return obj 
 }
複製代碼

update_ptr()函數,程序引用該對象

僞代碼以下:

update_ptr(ptr, obj){
    // obj 對象的計數器增量操做
    inc_ref_cnt(obj)
    // ptr 指針原來執行的對象計數器減量操做
    dec_ref_cnt(*ptr)
    // ptr 指正指向 obj
    *ptr = obj
}

// 該函數就是讓指針 ptr 要指向 obj 對象,實際中的代碼相似於:
/*
    var a = {};
    var b = {};
    a 對象計數器自增,b 對象計數器自減,
    b=a;
*/
複製代碼

inc_ref_cnt() 函數僞代碼實現:

// 這裏很簡單 ,就是計數器自增
inc_ref_cnt(obj){  
    obj.ref_cnt++ 
}
複製代碼

dec_ref_cnt() 函數僞代碼實現:

dec_ref_cnt(obj){
    // obj 對象計數器自減
    obj.ref_cnt--
    // 若是計數器等於 0 ,也就是說 obj 對象變爲垃圾了 
    if(obj.ref_cnt == 0)
  	    // 那麼被 obj 所引用的對象也應該作減量操做
        for(child : children(obj))      
    	    dec_ref_cnt(*child)
        // 將 obj 對象鏈接至空閒鏈表
    reclaim(obj) 
}
複製代碼

缺點

循環引用,形成內存沒法被回收

// 注意:例子僅用於描述場景,不符合真實環境狀況

funciton Person(name,lover) { 
  this.name = name;
  this.lover= lover;
}

let jxy = new Person("jxy"); 
let xxx = new Person("xxx")
jxy.lover = xxx;
xxx.lover = jxy;

xxx = null;
jxy = null;

// 當GC採用引用計數法管理內存的時候,在上面例子中雖然變量都被賦值爲空,可是兩個對象自己確相互
// 引用,這樣就致使了內存沒法被有效回收,即:內存泄露
複製代碼

GC複製算法

複製算法顧名思義,就是將堆中的全部活動對象複製到另一個空間,而後原來的空間所有回收掉。這樣的好處就是防止出現內存的碎片化,易於隨後爲程序分配新的空間。能夠形象的理解爲下圖:

image.png

複製算法

咱們把原來的活動對象空間稱之爲 **From **空間,將要複製到的新空間稱之爲 To 空間。當 **From **空間被徹底佔滿時,GC 會將活動對象複製到 To 空間。複製完成後,該算法會把 From 空間和 To 空間互換,本次 GC 也就結束了。GC 複製算法概要以下圖所示:

image.png

(GC 複製算法概要)


這裏再說明一下,**mutator **就是應用程序自己,一次回收完成以後,程序會繼續執行,再次產生垃圾,新的 From 空間會被填滿,而後 GC 又開始新的一輪迴收操做,回收操做伴隨程序的整個生命週期。

下面咱們經過僞代碼來的看一下 GC 複製算法的具體實現思路。

copying() 函數僞代碼:

copying(){	
    // 用 $free 變量記錄 To 空間的開始位置
    $free = $to_start
    // 遍歷全部的根對象,使用 copy 方法將他們複製到 To 空間
    for(r : $roots)
        // 返回的 *r 是對象被複制到 To 空間後新的指針。 
        *r = copy(*r)
    // 複製完成以後,交換 From 和 To 空間
    swap($from_start, $to_start)
}
複製代碼

**copy()**函數僞代碼:

/* copy 函數將做爲參數給出的對象複製,再遞歸複製其子對象 */
copy(obj){
    // obj 對象的 tag 域用於標記是不是一個已經被賦值過的對象 
    if(obj.tag != COPIED)
  	    // 使用 copy_data 方法具體來拷貝 obj 對象,同時傳入To 空間地址,以及 obj d對象的大小  
        copy_data($free, obj, obj.size)
        // 拷貝完成以後,標記一下,該對象已經被賦值過了. $free 變量指向新的 obj
        obj.tag = COPIED
        //  舊的 obj 對象的 forwarding 域保存新的 obj 對象的指針,用於後面將其賦給程序中原始的指向
        obj.forwarding = $free
        // $free 跳過已經被複制的 obj 的空間,指向 To 空間的空閒位置,方便下一次複製使用 
        $free += obj.size
        // 遞歸複製 obj 對象的引用對象
        for(child : children(obj.forwarding))
        *child = copy(*child)
    // 當拷貝完成以後返回新對象的指針
    return obj.forwarding
}
複製代碼

Chrome V8 的垃圾回收

前面寫了那麼多,那麼到底咱們經常使用的瀏覽器使用的是那種回收算法呢?我想這多是小夥伴們最關心的了。那麼以咱們最喜好的 Chrome 爲例,它使用的是多種回收算法的組合優化,而非某種單一算法。V8 的 GC 算法統稱爲分代垃圾回收算法,也就是經過記錄對象的引用次數,將超過必定引用次數的對象劃分爲老年對象,剩下的稱之爲**新生代對象,而後分別對他們採用不一樣到的垃圾回收算法。**那這樣劃分到底有什麼優點呢,咱們知道程序中生成的大多數對象其實都是產生以後隨即丟棄。如下面代碼爲例,函數內部生成了對象,在該函數執行完畢,出棧以後,包括函數自己以及它內部的變量都會馬上成爲垃圾:

// 該函數的執行上下文環境非全局做用域
function foo() {
    var a = {c:1};
    var c = {c:2};
}
複製代碼

那麼對於這種新生代對象來講,回收就會變得很頻繁,若是使用 GC 標記清除算法,那麼就意味着每次清除過程須要處理不少的對象,會浪費大量的的時間。因而若是對新生代對象採用 GC 複製算法的只須要將活動對象複製出來,而後將整個 From 清空便可,無需再去遍歷須要清除的對象,達到優化的目的。而針對老年對象則不一樣,它們都有多個引用,也就意味着它們成爲非活動對象的機率較小,也就能夠理解爲老年對象不會輕易變成垃圾。再進一步也就是老對象產生的垃圾不多,若是採用複製算法的話得不償失,大量的老年對象被複制來複制去也會增長負擔,因此針對老年對象採用的是標記清除法,須要清除的老年對象只是少數,這樣標記清除算法會更有優點**。**還有隨着程序的執行新生代的對象會變成老年對象,這個具體過程比較複雜,小的能力有限,這裏也就一筆帶過了。既然對象分爲新生帶對像和老年對象,那麼它們在堆中是如何分佈的呢,請看下圖:

image.png

(V8 的 VM 堆結構示意圖 )


這裏咱們只須要知道堆被分爲新生帶空間,和老年代空間便可。除了新生代空間中方的 **From **空間和 To 空間外,老年代空間中細分優化,各位大佬請自由探索,小的能有限,就不敢造次了o(╥﹏╥)o。

文章只是寫一下本身學到理解的東西,有錯誤還望大佬們指出啊。^-^

文章參考

相關文章
相關標籤/搜索