JVM之垃圾收集器與內存分配策略(三)

對於回收主要思考問題:
那些內存需要回收?
什麼時候回收?
如何回收?
來展開
第一個問題:那些內存需要回收?
即在哪些區域回收?在這些區域中那些內存可以回收?(對象是否已死?)

回收區域

虛擬機棧、本地方法棧、程序計數器這些都屬於線程私有區域,隨線程而滅,棧中的棧幀在類結構確定下來時就基本已經確定,因此有條不紊的進行出棧和入棧操作,方法結束或線程結束內存就自然回收了。因此關注的回收區域爲Java堆和方法區(主要是Java堆,回收效率高)
回收區域內存回收:
對於Java堆來說,主要回收已經無用的(死的)對象實例內存,所以要先解決那些對象已經死了或者可以回收了問題!
主要有兩種方法:
1》引用計數算法:給對象添加一個引用計數器,當有一個地方引用它時,計數器加1,引用失效時減1,當計數器爲0時表示沒有地方引用它。這種方法實現簡單,效率也很高,但是很難解決兩個對象之間相互引用問題。
2》可達性分析算法:從“GC Roots”對象作爲起點,開始搜索,搜索所走過的路稱爲引用鏈,如果一個對象到“GC Roots”沒有任何引用鏈,則表示這個對象不可達。(可以被回收的對象)。sun HotSpot 使用這種。其中可以作爲“GC Roots”的對象時:棧中本地變量表的引用對象、方法區類靜態屬性引用的對象,方法區中常量的引用対像,native方法引用的對象。
再談引用
強引用:Object a=new Object()強引用a只要還存在就不會回收a引用的對象。
軟引用:描述一些還有用但非必須的對象,這種是在內存即將發生溢出時,列爲回收對象進行二次回收(還沒有回收,回收有專門的線程)。如果回收後還沒有足內存則拋出內存溢出異常。
弱引用:描述非必須對象,這種弱引用關聯的對象只能活到下一次垃圾收集之前(這個收集並不一定是內存溢出前導致的)。
虛引用:最弱的一種引用關係,它的存在不會對其對象的生存時間產生影響,也無法通過虛引用來獲得一個對象實例。只是回收的時候對象可以收到一個通知。
對象的自我拯救:
即使是不可達的對象,也不一定“非死不可”,對象的死亡至少要經過兩次標記過程:如果與“GC Roots”之間沒有引用鏈,則做第一標記並且篩選該對象是否要執行finalize()方法,如果已經調用過該方法或者沒有覆蓋該方法則沒有必要執行。則會直接進入或等待第二次標記。如果需要自行,則會把它放在F-Queue的隊列中,專門一個線程去執行(觸發)它的方法,GC會對F-Queue中的對象第二次標記,如果這個時候建立有引用鏈,則就算逃脫,在第二次被標記的時候,移除“即將回收集合”,如果沒有建立引用鏈則就基本上被回收了。
不過這個方法不建議使用拯救對象。代價大,不確定性大。
這裏寫圖片描述
對於方法區來說:方法區回收效率低,主要回收兩部分:廢棄常量和無用的類。
常量回收:以常量池中的字母,字面量回收爲例,字符串“abc”已經在常量池中,如果沒有任何string對象引用常量池中的“abc”,那麼“abc”常量需要被回收,類似方法、字段的符號、接口等。
類(class對象)回收:需要滿足三個條件:
該類的所有實例對象都已經被回收,堆中不存在該類的任何實例。
加載該類的ClassLoader已經被回收
該類對應的java.lang.Class沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
同時滿足纔可以被回收,但不是一定會被回收。
什麼時候回收?上面回收區域也講到,對象等已經死掉!
如何回收?採用什麼方法回收?

垃圾收集算法

幾種垃圾收集算法:
1》標記-清除算法
第一階段“標記”需要回收的對象,第二階段“清除”回收對象區域。
缺點:效率不高,產生內存碎片
2》複製算法
原始是把內存分爲兩塊大小相等的區域,只使用其中的一半,當這一半用完的時候,進行這塊區域回收,把這塊中存存活的對象移動到另一半區域,並且按順序分配內存,這樣就不會產生內存碎片。
但這樣內存利用率太低,不過商用的虛擬機都用這種方法來回收新生代,不過不是按1:1劃分,而是分爲Eden和兩個survivor空間,HotSpot 默認比例大小eden:survivor=8:1, 這種需要老年代分配擔保。
3》標記-整理算法
複製算法並不適用老年代,因爲老年代存活率比較高,需要複製更多的對象,也不想浪費空間。(二者折中)所以提出 標記-整理算法,標記與“標記-清除”算法一樣,不過後面步驟不是直接清除對象內存,而是把所有存活的對象移動到連續的一塊,然後清理外面的區域。
4》分代收集算法
利用前幾種算法,根據對象存活的週期劃分內存爲幾塊:新生代、老年代
然後各區域使用合適的算法,比如新生代朝生夕死,採用複製算法,只有少量存活,則複製開銷比較小。老年代存活率較高,則使用“標記-清理”或“標記-整理”算法回收。
HotSpot算法實現
判定對象存活和垃圾收集算法實現時,有嚴格的考量:
枚舉根節點
可達性分析對執行時敏感體現在GC停頓上(stop the world),就需要時間凍結在某個點上,不能一邊分析,一邊還有對象引用關係變化。
安全點
HotSpot並沒有爲每個指令都生成OopMap,而是在“特定的位置”記錄這些信息,稱爲安全點,即程序不是在所有的地方都停頓下來GC,只有到達安全點才停頓。這些安全點特徵是長時間執行,比如循環跳轉、方法調用異常跳轉等。這些指令纔會產生安全點。
安全區域
對於指定的線程處於阻塞或睡眠狀態是,並沒有分配CPU時間,JVM顯然不能等這部分線程分配到CPU時間然後執行到安全點,對於這種情況,就需要安全區域來解決。
安全區域指這一段代碼中引用關係不會發生改變,在這裏開始GC是安全的,線程進入安全區則標識自己已經進入安全區域,這段時間發起GC(根節點枚舉)則不需要管自己,當自己要離開安全區域檢查系統是否已經完成根節點枚舉額,完成則繼續執行,否則等待可以離開的信號。
**

幾種垃圾收集器

**
HotSpot中的收集器
連線表示可以配合使用
這裏寫圖片描述

內存分配與回收策略

對象主要分配在新生代的Eden區,如果啓動本地線程分配緩衝區,則優先在TLAB上分配,少數情況也會直接分配到老年代。
1)對象優先分配在Eden區
對象大多數在新生代Eden區分配,如果Eden區沒有足夠空間則進行一次Minor GC,把存活對象存進survivor(兩部分來回複製),騰出的Eden區域用來分配新對象,如果這個時候Survivor空間還不夠分配,則使用分配擔保機制分配到老年代(這個機制是對於已經存活的對象區別於大對象直接進入老年代而沒有進行回收空間)。
2)大對象直接進入老年代
JVM提供一個-XX:PretenureSizethreshold參數,可以設置大於這個值就可以直接進入老年代分配,主要是避免在新生代使用賦值算法需要複製大量內存數據。
3)長期存活的對象進入老年代
JVM給每一個對象定義一個對象年齡計數器,在Eden區存活並且survivor可以容納,則年齡爲1,在survivor區每經過一次Minor GC年齡就加1,虛擬機默認年齡15進入老年代。
4)動態對象年齡判定
JVM並不是必須要年齡達到閾值纔可以晉升到老年代,如果survivor區中相同年齡的所有對象的和大於survivor區的一半,則大於或等於該年齡的對象直接進入老年代。
5)空間分配擔保
在發生Minor GC之前,虛擬機會先檢查老年代最大可用連續空間是否大於新生代所有對象的總空間,如果成立,那麼Minor GC是安全的。如果不成立,則查看是否允許擔保失敗,如果允許,查看老年代的連續空閒空間是否大於歷次晉升到老年代獨享的平均大小。如果大於,則嘗試進行一次Minor GC(有風險 失敗full GC),如果小於,或者不允許擔保失敗,則進行full GC。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章