GC 本來是一種「釋放怎麼都沒法被引用的對象的機制」。那麼人們天然而然地就會
想到,可讓全部對象事先記錄下「有多少程序引用本身」。讓各對象知道本身的「人氣指 數」,從而讓沒有人氣的對象本身消失,這就是引用計數法(Reference Counting),它是 George E. Collins
於 1960 年鑽研出來的。
引用計數法中引入了一個概念,那就是「計數器」。在對象頭中增長了一個計數器屬性,用來標識對象的被引用數量,也就是有多少程序引用了這個對象。node
本文代碼使用C語言實現git
對象在GC的世界裏,表明的是數據集合,是垃圾回收的基本單位。github
能夠理解爲就是C語言中的指針(又或許是handle),GC是根據指針來搜索對象的。算法
這個詞有些地方翻譯爲賦值器,但仍是比較奇怪,不如不翻譯……segmentfault
mutator 是 Edsger Dijkstra 琢磨出來的詞,有「改變某物」的意思。說到要改變什麼,那就是 GC 對象間的引用關係。不過光這麼說可能你們仍是不能理解,其實用一句話歸納的話,它的實體就是「應用程序」。
mutatar的工做有如下兩種:數據結構
mutator 在進行這些操做時,會同時爲應用程序的用戶進行一些處理(數值計算、瀏覽網頁、編輯文章等)。隨着這些處理的逐步推動,對象間的引用關係也會「改變」。伴隨這些變化會產生垃圾,而負責回收這些垃圾的機制就是 GC。
GC ROOTS就是引用的起始點,好比棧,全局變量函數
堆就是進程中的一段動態內存,在GC的世界裏,通常會先申請一大段堆內存,而後mutatar在這一大段內存中進行分配spa
活動對象就是能經過mutatar(GC ROOTS)引用的對象,反之訪問不到的就是非活動對象。翻譯
在引用計數算法中,使用空閒鏈表(free-list)的內存分配策略設計
空閒鏈表分配使用某種數據結構(通常是鏈表)來記錄空閒內存單元的位置和大小,該數據結構即爲空閒內存單元的集合。
在須要分配內存時,順序遍歷每個內存單元,找到第一個空閒的內存單元使用。
在本文中,爲了下降複雜度,只使用了最基本的free-list分配法,free-list數據結構以下圖所示:
爲了實現簡單,在本文代碼中,每一個單元只存儲一個對象,不考慮單元拆分合並等問題。
首先是對象類型的結構:
爲了動態訪問「對象」的屬性,此處使用屬性偏移量來記錄屬性的位置,而後經過指針的計算得到屬性
typedef struct class_descriptor { char *name;//類名稱 int size;//類大小,即對應sizeof(struct) int num_fields;//屬性數量 int *field_offsets;//類中的屬性偏移,即全部屬性在struct中的偏移量 } class_descriptor;
而後是對象的結構,雖然C語言中沒有繼承的概念,可是能夠經過共同屬性的struct來實現:
typedef struct _object { class_descriptor *class;//對象對應的類型 int ref_cnt;//對象被引用的次數,"人氣" } object; //繼承 //"繼承對象"需和父對象object基本屬性保持一致,在基本屬性以後,能夠定義其餘的屬性 typedef struct emp { class_descriptor *class;//對象對應的類型 int ref_cnt;//對象被引用的次數,"人氣" int id; dept *dept; } emp;
free-list結構設計
struct _node { node *next; byte used;//是否使用 int size;//單元大小 object *data;//單元中的數據 };
有了基本的數據結構,下面就能夠進行算法的實現了,如下執行GC前堆的狀態圖:
在其餘回收算法中,沒有空閒內存分配時會調用GC,回收那些已經時垃圾的對象內存。
然而在引用計數算法中並無明確啓動GC的地方。引用計數算法與mutator的執行關聯性強,在mutator的處理過程當中經過計數器的更新來進行內存管理;算是一種「實時」垃圾回收算法
引用計數算法中,有兩種狀況會更新對象的計數器,分別是建立對象/更新對象引用
和標記-清除算法同樣,須要先找到空閒的內存單元
node *find_idle_node() { for (next_free = head; next_free && next_free->used; next_free = next_free->next) {} //還找不到就觸發回收 if (!next_free) { gc(); } for (next_free = head->next; next_free && next_free->used; next_free = next_free->next) {} //再找不到真的沒了…… if (!next_free) { printf("Allocation Failed!OutOfMemory...\n"); abort(); } }
而後在找到的空閒內存單元中分配新對象,並初始化
object *gc_alloc(class_descriptor *class) { if (!next_free || next_free->used) { find_idle_node(); } //賦值當前freePoint node *_node = next_free; //新分配的對象指針 //將新對象分配在free-list的節點數據以後,node單元的空間內除了sizeof(node),剩下的地址空間都用於存儲對象 object *new_obj = (void *) _node + sizeof(node); new_obj->class = class; //初始化計數器 new_obj->ref_cnt = 0; _node->used = TRUE; _node->data = new_obj; _node->size = class->size; for (int i = 0; i < new_obj->class->num_fields; ++i) { //*(data **)是一個dereference操做,拿到field的pointer //(void *)o是強轉爲void* pointer,void*進行加法運算的時候就不會按類型增長地址 *(object **) ((void *) new_obj + new_obj->class->field_offsets[i]) = NULL; } next_free = next_free->next; return new_obj; }
更新對象引用,就是將對象引用的對象將A更新爲B,obj->ref_a = b
/** * 修改引用 * @param ptr 原指針,這個指針是引用的指針,pointer of pointer * @param obj 新對象指針 */ void gc_update_ptr(object **ptr, void *obj) { inc_ref_cnt(obj); dec_ref_cnt(*ptr); *ptr = obj; }
雖然在 mutator 更新指針時程序會執行此函數,但事實上進行指針更新的只有最後一哈昂行的 *ptr = obj 部分,其餘是進行內存管理的代碼
inc_ref_cnt是對指針 ptr 新引用的對象(obj)的計數器進行增長操做
void inc_ref_cnt(object *obj) { if (!obj) { return; } obj->ref_cnt++; }
dec_ref_cnt是對指針 ptr 以前引用的對象(*ptr)的計數器進行減小操做
void dec_ref_cnt(object *obj) { if (!obj) { return; } obj->ref_cnt--; //若是計數器爲0,則對象須要被回收,那麼該對象引用的對象計數器都須要減小 if (obj->ref_cnt == 0) { for (int i = 0; i < obj->class->num_fields; ++i) { dec_ref_cnt(*((object **) ((void *) obj + obj->class->field_offsets[i]))); } //回收 reclaim(obj); } }
在dec_ref_cnt方法中,首先對引用指針原有的引用對象計數器進行減小的操做。若是計數器減小後爲0,則該對象不可達了,沒有任何引用成了垃圾,須要被回收。
由於對象即將被回收,因此須要對這個對象全部的引用對象計數器也進行減小操做,並遞歸執行該邏輯。
以上就是對引用計數算法的說明
能夠及時回收垃圾,當對象的計數器爲0時,對象就會被回收;因爲單次回收的對象單一,因此mutator須要暫停的時間會很短,對應用形成的影響比較小;在此算法中不用遍歷對象圖來查找存活對象
每次對象關係變化,都須要更新計數器,更新過於頻繁;處理循環引用時較爲麻煩(有些資料上說引用計數沒法處理循環引用不太嚴謹,結合部分標記-清除算法就能夠解決此問題)
循環引用的例子:
上圖中,兩個對象互相引用,計數器都爲1;但對於GC ROOT都是不可達的,實際上應該是兩個非存活對象,但因爲互相引用,因此也會沒法回收
https://github.com/kongwu-/gc_impl/tree/master/reference-counting