雖然Java的垃圾回收機制已經十分優秀,但是爲了出現問題時,調試優化更容易,這裏繼續學習垃圾收集器和相關內存分配。
由於程序計數器、虛擬機棧、本地方法棧生命週期歲線程變化,因此是類結構確定下來時就已知的。因此這幾個區域的內存分派和回收都是確定的,不需要過多的考慮回收問題。
一、如何判斷對象死亡
1、 引用計數算法
當有一個地方引用該對象,計數器+1,當引用失效,計數器-1。任何時刻計數器爲0的對象不再可用。
無法解決對象間相互引用的問題,
2、 可達性分析
通過一系列稱爲GCRoots的對象作爲起始點,從這些節點開始向下搜索。所走過的路成爲引用鏈。當一個對象到GCRoots沒有任何引用鏈相連時,則稱爲不可達。
GC Roots:
1)虛擬機棧引用的對象
2)方法區中類靜態屬性引用的對象
3)方法區中常量引用的對象
4)本地方法棧JNI引用的對象
3、四種引用模式
1) 強引用:只要強引用存在,則永遠不會回收。
2) 軟引用:GC開始工作時,不一定會回收。在即將出現OOM時,將這類對象回收,如果內存還是不夠,則拋出OOM
3) 弱引用:只要當GC開始工作時,弱引用對象就會被回收。
4) 虛引用:虛引用唯一的目的是在對象被GC回收時收到一個系統通知。
4、如何判斷對象死亡
5、回收方法區
只有同時滿足以下三個條件纔可以被回收
1) 該類的實例全部被回收
2) 加載該類的ClassLoader被回收
3) Class對象沒有在任何地方被引用。
在大量使用反射、動態代理的場景可能涉及到類卸載的場景。
二、垃圾回收算法
1、 標記-清除算法
標記出所有需要回收的對象,標記完成後統一回收。
缺點:效率不高,會產生大量不連續的內存碎片
2、 複製算法
將可用內存分爲兩半,每次只使用一塊,當這塊內存用完了,將還存活的對象複製到另一半,然後將這塊內存所有對象清空。
缺點:只使用一般內存,代價比較高
常用於新生代回收。內存分爲Eden和兩塊Survivor。回收時將Eden和Survivor對象複製到另外一塊Survivor上。然後清楚這邊Eden和Survivor。一般比例爲Eden:Survivor=8:1.
3、 標記-整理算法
標記對象,將所有存活的對象移向一段,然後清理掉邊界外的內存。
4、 分代算法
根據對象存活週期不同分爲幾塊,不同分區採用不同的垃圾回收算法。
三、如何實現以上算法
1、 枚舉GC Roots
如果每次都需要重新搜索有哪些GC ROOTS ,如何建立引用鏈,那麼無疑會消耗太多無意義時間。因此在HotSpot中,使用OopMap記錄GC ROOTS。在類加載完成時,HotSpot就把對象相關信息記錄下來,在JIT編譯時也會記錄哪些位置是引用。所以當GC掃描時就可以直接獲得這些信息。
2、 安全點:
不能針對每條指令都生成對應的OopMap,所以只在特定的位置記錄這些信息,稱爲安全點。
如何讓GC發生時所有線程都在安全點停頓記錄OopMap呢:
搶先時中斷:GC發生時,將所有線程中斷,如果不在安全點,則讓它跑到安全點
主動式中斷:設置標誌位,線程輪詢,如果標誌爲真,則中斷掛起進入安全點
四、垃圾收集器
1、 Serial
後臺自動發起GC,暫停所有其他的工作線程,直到工作結束。
簡單高效,但是如果GC時間過程,會給用戶造成停頓時間過程,交互效果不好的影響
2、 ParNew
Serial的多線程版,同樣暫停用戶線程,但是GC可以並行執行垃圾回收操作
3、 Parrel Scavenge
關注於控制吞吐量的問題:CPU用於工作在用戶代碼時間和CPU總時間的比率。
適用於後臺計算的任務,充分利用CPU的工作。
設置最大垃圾收集停頓時間和吞吐量大小參數。如果停頓時間設置過小,導致新生代內存分配小,則GC發生的更加頻繁,導致吞吐量的下降。
4、 Serial Old
單線程收集器,Serial的老年版。
5、 Parallel Old
Parrel Scavenge的老年版,關注於吞吐量。
6、 CMS
獲得最短回收停頓時間爲目標的垃圾收集器。
清除過程:
初始標記:關閉其他線程,標記GC Roots能直接關聯到的對象。
併發標記:併發進行,進行GC Roots Tracing的過程。
重新標記:關閉其他線程,修正併發過程中由於變動的對象標記記錄。
併發清除:可以跟其他用戶線程一起執行。
缺點:
1) 併發操作佔CPU資源。
2) 需要預留內存給併發時產生的垃圾。
3) 由於基於標記-清除,產生大量內存碎片。
7、 G1
優點:
1) 並行和併發
2) 分代收集
3) 空間整合,採用標記-整理。
4) 可預測的停頓
首先G1的面向對象是整個內存區域,而不是新生代或者老年代。它將Java堆分爲多個大小相等的獨立區域,跟蹤每個區域的垃圾堆積的價值大小。在後臺維護一個優先列表。每次回收回收價值最大的區域。
運行過程:
1)初始標記:停頓其他線程,標記GC Roots能關聯到的對象
2)併發標記:併發執行,利用可達性找到存活的對象。
3)最終標記:停頓其他線程,修正併發期間更改的記錄。
4)篩選回收:更具回收價值,篩選回收區域,
五、內存分配和回收策略
1、對象優先在Eden分配
當Eden沒有足夠空間分配時,發起一次Minor GC。
2、大對象直接進入老年代
設定一個-XX:PretenureSizeThreshold參數,大於這個參數的對象直接進入老年代
3、長期存活的對象進入老年代
對象在Eden經歷一次Minor GC並被移入Survivor區時,設置對象年齡爲1,此後每經歷一次MinorGC,年齡加一。設置-XX:MaxTenuringThreshold,年齡超過這個值進入老年代。
4 動態年齡對象判定
設置如果對象年齡達到某個值的對象佔比超過50%,則移入老年代。
5 空間分配擔保
發生Minor GC之前,虛擬機檢查老年代最大可用連續空間是否大於新生代所有對象的空間,如果是,這次Minor GC是安全的。
如果不成立,看看是否允許擔保失敗,如果允許,查看老年代最大可用連續空間是否大於是否大於晉升到老年代對象的平均大小,如果大於,嘗試進行Minor GC。如果小於或者不允許冒險,則進行Full GC。
關於冒險的解釋:由於新生代採用複製算法,在極端條件下,另一個Survivor的大小不足以保存活下來的對象,這時存放在老年代裏。如果老年代空間還不夠,則需要發生一次Full GC。而老年代的冒險就是爲新生代的存活對象擔保存放對象。