在上一篇文章中,介紹了在GC機制中,GC是以什麼標準斷定對象能夠被標記的,以及最有效最經常使用的可達性分析法。
今天介紹另一種很是經常使用的標記算法,它的應用面也至關普遍。這就是:
引用計數法 Reference Counting
這個算法的本質,其實就是上篇文章中判斷一個對象要被回收的另一種思路,即若是沒有其它對象調用當前對象,那麼當前對象就能夠被回收了。判斷有多少調用當前對象有兩種方法,一種是看看其它對象,有多少對象持有當前對象的引用。還有一種辦法就是,當前對象自身實現一個計數機制。統計來自外界引用的調用。第一個辦法就是上篇文章中可達性分析的最初思路。而第二個辦法就是如今要介紹的引用計數法的最初思路:咱們不關心誰保存了咱們的引用,咱們只關心保存咱們引用的對象究竟有多少個。
在引用計數法中,每一個對象擁有一個記錄自身引用被持有的個數,當這個對象的計數器的值爲0時,也就是再也不有其它對象持有該對象的引用了,那麼也就是再也不有對象能夠調用到當前對象的方法或者變量了。這一刻也就是當前對象能夠被回收的時刻了。
在《垃圾回收的算法與實現》這本書中,對於該算法又一個頗有意思的描述;
每一個對象就像是一個明星。這個對象的引用計數的大小,就像是這個明星的人氣指數。當明星的人氣指數爲0時,也就是這個明星黯然離場的時候了。
在引用計數法中,每一個對象的引用計數器初始(防盜鏈接:本文首發自http://www.cnblogs.com/jilodream/ )值爲0。沒當有一個新對象持有當前對象的引用時,計數器就會加1。沒當有一個已經持有引用的對象消失,或者拋棄持有的引用時。計數器就會-1。當計數器的值再次爲0,這個計數器所表明的對象就會被回收掉。(更準確的說,是會讓空閒鏈表持有本身的引用,將本身所佔用的內存空間標記爲能夠從新分配的區域)。
接下來講說這個算法的優缺點:
優勢:
一、隨時隨地的回收垃圾
當計數器變爲0的瞬間,當前對象就會被放置到空閒隊列中,做爲能夠被從新分配的內存空間。而其餘的GC算法,如以前講到的可達性分析法,都須要進行一次全局的清理,纔會統一的清理掉這個週期內的全部已知的垃圾空間。
二、最大暫停時間短
引用計數法師在每次生成、或銷燬對象或者是變動指針的時候進行一次計算的,所以對程序的影響時間是很是短暫的。
而其餘的GC算法則因爲須要統一的清除或者是複製等,因此暫停的時間會比較長,對程序的影響也比較長。有時這個時間長到性能上已經沒法忍受時,就須要不斷的調優,減短單次暫停的最大時長。
三、核心思路簡單
引用計數法。不須要從根節點依次開始進行遍歷。每一個對象只關心直接持有本身引用的對象是否發生了變化。這樣當對象發生回收時,也隻影響這個對象直接持有的引用對象,而不會直接影響到更深路徑的對象。
如A持有B/C,B持有D/E。當A被回收時,只會影響B,C對象的引用計數器。當B的計數器值所以降爲0時,纔會影響到D/E節點。總體的計算成本很是的低。而其餘的GC方式須要從根依次進行遍歷。整個過程很是複雜(如涉及到一個網狀的引用關係,如何終止掉無心義的遍歷就尤其重要)。
缺點:
一、計算的頻率過快
每次執行一條命令時,均可能會引發若干次的引用計數變化。尤爲是對於一些根節點持有的對象(從根對象)的引用計數,其變化的速度更是驚人。所以計數器的工做量很是繁重。
二、計數器所佔用的內存空間很是的大
引用計數法中。每一個對象都須要一個屬於本身的引用計數器,儘管這個計數器使用無符號型來存儲,可是也只能節約一個bit位的空間。因爲可能會發生全部對象都持有一個對象的極端條件,因此計數器所容許的最大值必定是要很是大。(防盜鏈接:本文首發自http://www.cnblogs.com/jilodream/ )相應的所佔用的控件也會很是的大。這是一種是典型的空間換取時間的算法。
三、實現很是複雜,一旦出錯後果會很是的嚴重
儘管該算法的優勢是思路很是簡單,可是實現起來卻要複雜的多。每當一個對象回收時,都須要刷新每一處使用到的對象的計數器。一旦有一處錯誤,則可能會出現永遠沒法修復的內存泄露問題。
四、循環依賴
這個問題能夠是引用計數法被公認的最難處理問題:當兩個(也可使是多個)內存對象互相依賴,同時也不與外界有引用關係,從而造成一種相似孤島鏈的關係。此時每個對象計數器都不爲0,GC也就沒法回收掉這些內存了。這種狀況下,典型的引用計數法是沒法解決掉的。每每須要結合其餘的回收算法,進行改良才能解決問題。算法
針對這些缺點。業界提供了不少的改良算法
一、延遲引用計數法 Deffered Reference Counting
deferred [dɪ'fɜ:d] adj. 延期的,緩召的;
針對從根對象的引用很是頻繁的更新,從而致使其計數器的計算任務很是繁重的這個問題。有人提出了一種特別的思路:不維護從根引用對象的計數器。這些計數器的值始終爲0。其餘對象仍然正常採用引用計數器的方式。可是這就會有一個問題,GC沒法判斷哪些對象是能夠回收的,哪些是不能回收。所以就須要把計數器降爲0(decr_ref_cnt函數)的對象暫時先放置在一個容器中,延遲它的回收。這個容器稱爲ZCT(Zero Count Table)。它專門用來記錄那些計數器通過減持計算而變爲0的對象。函數
以下圖:性能
那麼何時開始真正的標記垃圾對象呢?
通常來講當咱們建立(new_obj函數)對象時,發現已經沒有空餘的內存空間能夠分配時。就會進行一次ZCT掃描(scan_zct函數)來清理掉這些對象。而後再次嘗試分配內存,若是仍然不能成功,那麼這時候就認爲內存溢出了。
ZCT掃描(scan_zct函數)的步驟以下:
(1)首先將從根引用的計數器調整到正常的數值;
可見下圖:指針
(2)而後遍歷ZCT,將值爲0的對象都清理掉(delete(obj)函數),放置到前文說的空閒隊列中。(此時這些對象的空間就能夠被用來分配新的對象了)
(3)而後將全部的從根引用的計數器再調整回去。對象
這個算法的好處是,廢棄掉從根引用對象的計數器被頻繁刷新這些無心義的繁重耗時的操做,大大減輕了處理器的負擔。
固然它的缺點也很明顯,內存再也不會被當即回收掉。只有當內存空間不夠時(防盜鏈接:本文首發自http://www.cnblogs.com/jilodream/ ),纔開始掃描ZCT,統一進行回收。而掃描ZCT的耗時通常會隨着ZCT的增大而增大,這樣就致使了GC的最大的暫停時間變大。固然也能夠經過調小ZCT來減少最大暫停時間,可是這樣又會讓GC更頻繁的進行ZCT掃描(空間與時間不可兼得)。從而致使內存回收處理的吞吐量降低。二、Sticky引用計數法
sticky 英 [ˈstɪki] adj. 粘性的; 不動的;
前文有提到計數器的控件佔用很是的大。這是爲了保證極端場景計數下,計數器能夠正常使用。但這種算法卻偏偏是將計數器所佔用的空間(計數上限)縮小。這是由於對於大部分對象來講,計數器中所能達到的最大值都不大,對象很快就會被回收了。爲了保證計數器每個對象的計數器都不會溢出,而給每個對象都開闢一塊很是大的空間來計數,這是一種很是愚蠢的行爲。
對於個別計數器會溢出的對象來講:
(1)那麼就讓它溢出好了
反正它都被這麼多對象引用了,機率上講,基本也不太會被回收了,能夠說默認它爲永生對象了。
(2)若是認爲計數器溢出很差,能夠加入從根尋址的變相算法,大體思路是這個樣子的:
<1>計數器歸0
<2>從根依次尋址,增長引用計數值
<3>清理掉引用計數器爲0的對象
這個算法的好處是:
<1>下降計數器佔用的空間
<2>清理掉循環引用的場景
三、1位引用計數算法
這個算法能夠說是Sticky引用計數法的一種極端體現,也就是計數器只有1位。一旦出現共有一個內存的場景下就「溢出」了。
儘管場景很極端,可是他表明了不少的內存:這些對象從創造出來以後,只被一個對象持有,不存在多個對象共同持有的狀況。
這種算法中,計數器更多像是一個標記值,標記當前對象是被其餘對象引用,而再也不是一個對象的計數器。
四、部分標記清除算法
這個算法能夠說是純粹就是爲了解決循環依賴的。算法的大體思路以下:將內存對象標記爲四種狀態:
A絕對不是垃圾的對象;
B絕對是垃圾的對象;
C可能存在循環引用的對象;
D搜索完畢的對象。
這樣就能夠針對對象的狀態採用不一樣的回收算法計算。因爲內存中的大部分對象都處於循環引用的孤島連以外(防盜鏈接:本文首發自http://www.cnblogs.com/jilodream/ )。所以大部分的對象仍然採用的是引用計數法進行計算。只有少部分的對象可能存在循環引用中,所以只對這部分對象進行進行根可達性的計算。
儘管引用計數法自身存在諸多的缺陷,可是仍然有不少地方採用了這種回收算法:如Python的GC、Flash player的內存管理等。惋惜因爲循環依賴等緣由,到目前爲止,主流的JVM還均沒有將引用計數法做爲GC的回收算法來使用。blog
對於這篇文章中提到的這些GC標記算法,以及這些GC標記算法的實現,在這裏推薦一本前文提到的書:《垃圾回收的算法與實現》。這本書寫的很是詳細,書中的插圖也很是形象。有興趣的同窗能夠找來閱讀下。隊列