GC 全稱 Garbage Collection ,即:垃圾回收。javascript
寫過計算機程序的都知道,程序中無時無刻伴不隨着變量的引用,賦值,運算等操做。因而乎存在着某些變量在使用事後,程序不會再用到它們,可是他們依然佔據着必定的內存空間。內存中這樣不會被程序再次使用的數據即可稱之爲‘垃圾’。java
簡而言之,回收就是將上面講到的 程序運行時產生的‘垃圾’ 釋放掉,以便於程序再次使用這塊內存區域。算法
使人驚歎的是,目前全部的GC算法只不過是上述3種算法的組合或應用,也就是說從1963年賦值算法誕生時,到目前爲止沒有誕生新的GC算法!瀏覽器
首先咱們明確個知識點,GC在內存中銷燬移動的基本單位是對象。
bash
對象由頭(head)和 域(field)構成。
函數
對象中保存對象自己信息的部分稱之爲‘頭’。head中的信息用戶沒法訪問和修改,包含下列信息。優化
field 對於咱們而言比較熟悉,在JavaScript使用屬性操做符即可以直接訪問的部分被稱之爲域。域包含兩種數據類型。ui
指的是修改CG對象之間的引用關係。更準確的來說它就是 ‘應用程序’自己,也就是咱們寫的代碼。
this
堆指的是用於動態(也就是執行程序時)存放對象的內存空間。當 mutator 申請存放對象時, 所需的內存空間就會從這個堆中被分配給 mutator。spa
也就是分配到內存空間中的對象中那些可以經過 mutator 引用的對象稱之爲 ‘活動對象’,反之爲‘非活動對象’即:垃圾。
在CG世界中,根指的是對象指針的起點部分。也就是**mutator **(應用程序) 中的全局變量,經過遞歸這些根(全局變量)能夠遍歷到的對象就是活動對象,反之就是非活動對象(垃圾)。
如該算法的名稱所描述,它的執行能夠分爲兩個階段,即:標記 和 清除。標記就是經過上面提到的根(全局對象)能遍歷到的內存中的對象標記爲活動對象,標記完成以後接下來就清除內存中沒有被標記的對象,也就是非活動對象。經過這兩個階節階段實現了內存空間的重複利用。
標記過程實際上能夠經過僞代碼來更加具體展示:
/*標記函數*/
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 回收事後堆的狀態:
GC 被本質上來講就是一種研究如何釋放沒法被引用對象的技術。那麼,能夠想到,若是讓對象本身記錄一下,它有沒有被程序引用。這就是引用計數法。
new_obj()
函數和
update_ptr()
函數。
僞代碼以下:
new_obj(size){
// 從‘空閒鏈表’中爲程序分配空間
obj = pickup_chunk(size, $free_list)
// 分配空間失敗
if(obj == NULL)
allocation_fail()
else
// 爲分配成功的對象設置計數器,並初始化
obj.ref_cnt = 1
return obj
}
複製代碼
僞代碼以下:
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採用引用計數法管理內存的時候,在上面例子中雖然變量都被賦值爲空,可是兩個對象自己確相互
// 引用,這樣就致使了內存沒法被有效回收,即:內存泄露
複製代碼
複製算法顧名思義,就是將堆中的全部活動對象複製到另一個空間,而後原來的空間所有回收掉。這樣的好處就是防止出現內存的碎片化,易於隨後爲程序分配新的空間。能夠形象的理解爲下圖:
咱們把原來的活動對象空間稱之爲 **From **空間,將要複製到的新空間稱之爲 To 空間。當 **From **空間被徹底佔滿時,GC 會將活動對象複製到 To 空間。複製完成後,該算法會把 From 空間和 To 空間互換,本次 GC 也就結束了。GC 複製算法概要以下圖所示:
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 的 GC 算法統稱爲分代垃圾回收算法,也就是經過記錄對象的引用次數,將超過必定引用次數的對象劃分爲老年對象,剩下的稱之爲**新生代對象,而後分別對他們採用不一樣到的垃圾回收算法。**那這樣劃分到底有什麼優點呢,咱們知道程序中生成的大多數對象其實都是產生以後隨即丟棄。如下面代碼爲例,函數內部生成了對象,在該函數執行完畢,出棧以後,包括函數自己以及它內部的變量都會馬上成爲垃圾:
// 該函數的執行上下文環境非全局做用域
function foo() {
var a = {c:1};
var c = {c:2};
}
複製代碼
那麼對於這種新生代對象來講,回收就會變得很頻繁,若是使用 GC 標記清除算法,那麼就意味着每次清除過程須要處理不少的對象,會浪費大量的的時間。因而若是對新生代對象採用 GC 複製算法的只須要將活動對象複製出來,而後將整個 From 清空便可,無需再去遍歷須要清除的對象,達到優化的目的。而針對老年對象則不一樣,它們都有多個引用,也就意味着它們成爲非活動對象的機率較小,也就能夠理解爲老年對象不會輕易變成垃圾。再進一步也就是老對象產生的垃圾不多,若是採用複製算法的話得不償失,大量的老年對象被複制來複制去也會增長負擔,因此針對老年對象採用的是標記清除法,須要清除的老年對象只是少數,這樣標記清除算法會更有優點**。**還有隨着程序的執行新生代的對象會變成老年對象,這個具體過程比較複雜,小的能力有限,這裏也就一筆帶過了。既然對象分爲新生帶對像和老年對象,那麼它們在堆中是如何分佈的呢,請看下圖: