所謂垃圾收集器的作用就是回收內存空間中不需要了的內容,需要解決的問題是回收哪些數據,什麼時候回收,怎麼回收。
Java虛擬機的內存分爲五個部分:程序計數器、虛擬機棧、本地方法棧、堆和方法區。
其中程序計數器、虛擬機棧和本地方法棧是線程私有的,所以對於何時回收這三部分內存只需要根據線程的生存週期就可以了。
而堆和方法區是線程共享的,其誕生和銷燬伴隨的虛擬機的啓動和停止,所以需要特定的算法來判斷內存是否可以被回收。
垃圾回收器在回收之前,需要判斷那些對象已經不會被使用,那些是需要被使用的。對於那些無效的對象,有兩種判定算法:
比較:引用計數法雖然簡單,但是無法解決循環引用的問題,目前主流的語言採用可達性方法。
要真正宣告一個對象的死亡,至少需要進行兩次標記:當在可達性算法標記之後,發現對象沒有引用鏈,就被第一次標記,隨後進程篩選,如果對象的finalize方法沒有被重寫或者finalize方法被調用過了,就進行第二次標記,這樣對象就死了。
如果finalize方法沒被調用,虛擬機就會給對象一次重生機會:
注意:
強烈不建議使用finalize()函數進行任何操作!如果需要釋放資源,請使用try-finally。
因爲finalize()不確定性大,開銷大,無法保證順利執行。
由於方法區中存放的信息生命週期較長,每次回收的信息較少,回收的性價比較低,所以方法區就是堆的老年代。
方法區的垃圾收集主要分爲兩部分內容:
和清除對象類似,如果常量沒有被引用,則被清理出常量池。
同時滿足三個條件:
當我們知道內存中哪些區域需要被回收,那麼就需要垃圾回收算法來清理這些數據。
分代收集實質是一套符合大多數程序運行實際情況的經驗法則,建立在兩個分代假說之上:
這兩個分代假說奠定了垃圾收集的一致的原則設計:收集器應該將Java堆分爲不同的區域,然後將對象依據其年齡,將其分配到不同的區域中進行存儲。
Java虛擬機至少把Java堆分爲新生代和老年代兩個區域。新生代中,每次垃圾回收都會有大量的對象被死去,然後存活少量的對象,將會逐步晉升到老年代中。
新生代中的對象可能會被老年代中的所引用,爲了找出新生代中存活的對象,不但要從GC Roots開始掃描,而且還需要遍歷整個老年代來獲得引用節點,這樣爲了少量跨代區掃描整個老年代,性價比太低。所以,只需要在新生代中建立一個全局的數據結構記憶集,這個結構把老年代分爲若干塊,並標識出那些塊會存在跨代引用,在GC掃描的時候,掃描這一部分老年代就好了,這樣節約時間。
首先標記所有需要回收的對象,然後統一回收被標記的對象,清楚數據。
缺點:執行效率不穩定,標記和秦楚的效率都隨着對象數量的增長而降低,而且會產生內存碎片,無法存儲較大的對象。
將內存分爲兩個大小相等的部分,每次使用其中一塊,如果這一塊使用完了,就將存活的對象複製到另一塊,然後將這一塊的數據清除。
優點:不會產生內存碎片。
缺點:浪費了一半的內存空間,而且每次要將可用數據複製一下,效率低。
現在絕大多數的虛擬機採用這一算法來回收新生代,因爲新生代存活的對象少,每次需要複製的數據就比較少。
**解決空間利用率低的問題:**重新佈局新生代,將其分爲一塊較大的Eden空間(伊甸園)和兩塊較小的Survivor1和Survivior2空間,HotSpot對其默認比例是8:1:1。分配內存時,只使用Eden和Survivor1,發生垃圾收集時,將Eden和Survivor1中任然存活的的對象複製到Survivor2上,然後直接清理原有的兩個空間,然後將Survivor2 與Survivor1的空間引用互換,繼續分配。
這樣做,只有10%的新生代空間被浪費。
當Survivor空間不足以容納一次Minor GC之後的存活對象,就需要依賴其他區域進行分配擔保。所謂分配擔保,就是當Survivor區域內存不夠,向老年代借一些空間從來存儲對象。
在垃圾回收之前,將需要回收的垃圾進行標記,然後在垃圾回收的時候,將沒有被標記的數據放到一端,最後清除另一端的數據。
這是一種老年代的垃圾收集算法。
標記-清除算法與標記-整理算法的本質差異在於前者是一種非移動式的回收算法,而後者是移動式的。移動和不移動都會有優缺點,移動會耗時,不移動造成內存碎片,降低吞吐量。移動則內存回收時會更復雜,不移動則內存分配時會更復雜。
關注吞吐量就使用標記整理算法,關注延遲就使用標記清除算法。
吞吐量是指對網絡、設備、端口、虛電路或其他設施,單位時間內成功地傳送數據的數量。
讓虛擬機平時多數時間都採用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經大到影響對象分配時,再採用標記-整理算法收集一次,以獲得規整的內存空間。前面提到的基於標記-清除算法的CMS收集器面臨空間碎片過多時採用的就是這種處理辦法。