文章目錄
垃圾收集的作用
很明顯你會回答通過清除不用的對象來釋放記憶體,但是別忘了垃圾收集的另外一個重要作用就是消除堆記憶體空間的片段。
垃圾收集策略引用計數
這是早期策略。非常簡單,對象A被引用,則它的計數Acount就加1,當對A的引用失效了,Acount就減1,當Acount=0時,就可以對A進行記憶體回收。對A進行記憶體回收時,A中引用的其他對象的計數都減1,因此A的回收可能會導致連鎖反應。
優點:簡單,快
缺點:無法檢測循環參考,比如A的子類a引用了A,A又引用了a,因此A和a永遠不會被回收。這個缺點是致命的,因此現在這種策略已經不用。
跟蹤收集器
又稱為“標記並清除”策略。基本思想是從根對象開始遍曆整個對象圖,仍然被引用的對象打上標記,遍曆結束後,沒有被打上標記的對象就可以清除。記錄標記的策略可以在對象本身記錄或者通過位元影像來記錄。
我的疑問:會不會造成“誤殺”啊?標記之後清除之前一個沒有被打上標記的對象又突然被引用了怎麼辦?
這個問題想了一下應該不會,因為清除時只針對當前對象圖這個範圍進行,一個對象由沒有被引用到被引用勢必會擴大對象圖,而這不在本次清除處理的範圍之內。
跟蹤收集器通常使用兩種策略來實現:
1.壓縮收集器:遍曆的過程中如果發現對象有效,則立刻把對象越過空閑區滑動到堆的一端,這樣堆的另一端就出現了一個大的連續空閑區,從而消除了堆片段。
2.拷貝收集器:堆被分為大小相等的兩個地區,任何時候都只使用其中一個地區。對象在同一個地區中分配,直到這個地區被耗盡。此時,程式執行被中止,堆被遍曆,遍曆時被標記為活動的對象被拷貝到另外一個地區。這種做法用稱之為“停止並拷貝”。
這種做法的主要缺點是:太粗暴,要拷貝就都拷貝,粒度太大,整體效能不高。因此就有了更先進的“按代收集的收集器”
按代收集的收集器
基於兩個事實:
1)大多數程式建立的大部分對象都有很短的生命週期。
2)大多數程式都建立一些具有非常長生命週期的對象。
因此按代收集策略就是在“停止並拷貝”策略基礎之上,把對象按照壽命分為三六九等,不是一視同仁。它把堆劃分為多個子堆,每一個子堆為一“代”物件服務。最年幼的那一代進行最頻繁的垃圾收集。沒經過一次垃圾收集,存活下來的對象就會“成長”到更老的“代”,越是老的“代”對象數量應該越少,他們也越穩定,因此就可以採取很經濟的策略來處理他們,簡單“照顧”一下他們就行啦。這樣整體的垃圾收集效率要比簡單粗暴的“停止並拷貝”高。
火車演算法
說火車演算法之前要先說一下“漸進收集”,按代收集已經比“停止並拷貝”更高效,但是按代收集每次處理的物件範圍仍然是整個“堆”,有些情況下,記憶體回收的開銷而造成的系統響應遲鈍仍然讓使用者無法忍受,因此就提出了“漸進收集”,採取小步快跑的方式,每次只處理一部分“堆”空間的資料,“神不知鬼不覺”地完成垃圾收集工作。
下面說說火車演算法,首先明確一個問題:火車演算法要解決什麼問題?
火車演算法是用來替代按代收集策略的嗎?不是的,可以說,火車演算法是對按代收集策略的一個有力補充。我們知道按代收集策略把堆劃分為多個”代“,每個代都可以指定最大size,但是”成熟對象空間“除外,”成熟對象空間“不能指定最大size,因為它是”最老“對象的最終也是唯一的歸宿,除此之外,這些”老傢伙“無處可去。而你無法確定一個系統最終會有多少老對象擠進”成熟對象空間“。
火車演算法詳細說明了按代收集的垃圾收集器的成熟對象空間的組織。火車演算法的目的是為了在成熟對象空間提供限定時間的漸進收集。
火車演算法把成熟對象空間劃分為固定長度的記憶體塊,演算法每次在一個塊中單獨執行。為什麼叫”火車演算法“?這與演算法組織這些塊的方式有關。
每塊資料相當於一節車廂
每一個資料區塊屬於一個集合,集合內的所有資料區塊已經進行排序,因此集合就好比是一列火車
成熟對象空間又包含多個集合,因此就好比有多列火車,而成熟對象空間就好比是火車站。
記憶體回收-火車演算法-成熟對象區記憶體組織形式
火車演算法根本思想還是”分而治之“,目的是達到漸進回收,演算法細節,下次再詳細描述一下。
======================================
火車演算法的大致執行過程如下:
1)首先檢查序號最小的整列火車,如果已經不存在任何引用(成熟對象區之內的&之外的)指向這列火車中任何車廂中的對象,則直接回收整列火車的記憶體;
2)如果1)的檢查結果為false,則注意力集中到序號最小火車的序號最小的車廂上,如果整節車廂的對象都不再被引用,則直接回收;
3)如果序號最小的車廂中的對象還有被引用的,則進行對象轉移。如果引用來自其他火車的某個車廂,則把當前對象轉移到引用它的那節車廂,如果那節車廂空間不足,則增加新的車廂,並在新的車廂中存放轉移來的對象。如果引用來自成熟對象空間之外,則對象被轉移到其他火車。如果引用來自本火車的其他車廂,則對象被轉移到火車最末尾的車廂。
4)經過3)的對象轉移,還剩下的對象就可以進行記憶體回收了。
通過上面的描述,我們會注意到,火車演算法在進行記憶體回收時要掃描目標對象,確保沒有對象引用指向他們,這是比較耗時的工作,因此火車演算法採用了記憶集合來記錄所有對一節車廂或者一列火車所有的外部參考。如果發現記憶集合為空白,則說明沒有引用,直接可以回收車廂或者火車。
finalize方法
作用:一個鉤子方法,jvm在對當前對象執行垃圾收集之前,可以通過調用finalize方法來作一些額外的工作。注意:finalize方法是由jvm來調用的。
另外一點需要明確的是finalize方法會增加記憶體回收的複雜度。大致過程是這樣的:
1)垃圾收集器通過某些演算法找出所有沒有被引用的對象,並準備對其進行回收(這個過程稱之為“第一趟掃描”);
2)垃圾收集器此時要挨個判斷即將被回收的對象是否擁有finalize方法,如果有,則調用它。
3)執行了所有finalize方法之後,垃圾收集器必須再次進行一趟掃描(稱之為“第二趟掃描”),來判斷即將被回收的對象是否真的應該被回收,因為finalize方法可能會“複活”某些對象,因此這趟掃描是必要的。
關於finalize還有一點需要記住:一個對象的finalize只可能被運行一次(如果提供了的話)。這是因為jvm會記住某對象的finalize方法是否已經被執行過,如果已經被執行過,但是對象仍然有效(比如finalize方法把它自己“複活”了),那麼當這個對象再次變得無效時,jvm回收該對象時就不會再執行它的finalize方法了,jvm會把它當作沒有提供finalize方法的對象來看待。否則該對象將永遠不會被回收了。
對象可觸及性的生命週期
在垃圾收集器看來,堆中的每個對象都有6種狀態:
強可觸及、弱可觸及、軟可觸及、影子可觸及、可複活、不可觸及。
其中強可觸及與較弱的形式(弱可觸及、軟可觸及、影子可觸及)直接的差別是前者絕對不允許引用的目標被記憶體回收,而後者則允許。
可複活的:雖然從根節點的對象圖中不可觸及,但是有可能從記憶體回收行程執行某些終結方法時可以觸及。
不可觸及的:已經沒有其他對象引用的。
搞出了這麼多狀態,有什麼用呢?
簡單點說,軟引用使你可以建立記憶體中的緩衝,它與程式的整體記憶體需求有關。弱引用使你可以建立規範映射,比如雜湊表,它的關鍵字和值在沒有其他程式部分的引用時可以從映射中清除。影子應用使你可以實現除finalize方法之外的更加複雜的臨終清理政策。
軟引用比較好理解,這裡比較難理解的是弱引用,其實只要舉一個具體的例子就明白了:
我們經常使用生命週期與整個應用程式生命週期一樣長的Map<xxBean,yyBean>來作為緩衝持有一些中繼資料,如果我們使用的是HashMap<xxBean,yyBean>(強引用),那麼就會導致Map中的所有key-value的生命週期與Map的生命週期一樣長,因為這是強引用。這就為記憶體泄露埋下了伏筆,比如某一個key:xxBean其實已經不再使用,但是由於其所屬的Map一直被引用,就導致這個xxBean也一直不會被記憶體回收。而如果此時我們使用WeakHashMap來代替HashMap就可以解決這個問題,垃圾收集器一旦發現除WeakHashMap還持有對xxBean的引用的話,它就會毫不猶豫地釋放到xxBean-yyBean這個索引值對。
關於軟引用、弱引用和影子引用最後再說一點:由於垃圾收集器可以自行決定他們的命運,因此java提供了引用隊列來讓開發人員可以監聽到垃圾收集器對這些對象的處理。
下面的API必須知道:
java.lang.ref.Reference
關於弱引用的一個例子:http://www.ibm.com/developerworks/cn/java/j-jtp11225/