JVM-垃圾回收
1.垃圾回收如何判定
1.1引用計數法
引用計數法是給對象添加一個引用計數器,當有對該對象的引用時,計數器加1,引用失效時,計數減1,計數器為0時不能再使用.該對象可以被垃圾回收器回收,但是存在一個問題,就是當兩個對象相互進行引用時,它們的計數器最終都不會為0,導致垃圾回收器無法回收它們。
1.2可達性分析算法
算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
Java語言中,可作為GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)引用的對象
1.3判斷當前引用類型
Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、 軟引用(Soft Reference)、 弱引用(Weak Reference)、 虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱
強引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引
軟引用是用來描述一些還有用但並非必需的對象。 對於軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。 如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。 在JDK 1.2之後,提供了SoftReference類來實現軟引用。
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。 當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。 在JDK 1.2之後,提供了WeakReference類來實現弱引
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。 一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。 為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。 在JDK 1.2之後,提供了PhantomReference類來實現虛引用
1.4判斷對象是否可以回收
真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。 當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。
1.5針對永久代的垃圾回收
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
廢棄常量:沒有任何對象是當前String類型的常量,比如說當前String s = "adc",當沒有對象引用"adc"時,該常量就可以被垃圾回收器回收
廢棄的無用類:Java堆中不存在該類的任何實例
加載該類的ClassLoader已經被回收;
java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
2.垃圾回收算法
2.1標記-清除算法
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象
優點:實現原理簡單
缺點:標記和清理時效率不高;清楚過程中會產生內存碎片。
2.2復制算法
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。 當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用過的內存空間一次清理掉
優點:實現簡單,運行高效
缺點:浪費了一半的內存,需要將內存縮小為原來的一半來復制存活的對象區域。
2.3標記-整理算法
標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
2.4分代收集算法
根據對象存活周期的不同將內存劃分為幾塊。 一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。 而老年代中因為對象存活率高、 沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收
新的對象分配在Eden 和 Survivor1 這兩個空間中如果空間不足, 發起GC, 將 Eden , Survivor1中的存活對象移動到Survivor2中然後將Eden, Survivor1中對象清理掉。 -- 如果Survivor2的空間也不夠,則依賴其他內存(老年代)接下來新對象會被分配在Eden 和Survivor2中, 即 Survivor1 變成了完全空閑空間,它和Survivor2的角色進行了轉換 , 如此循環下去。
3.垃圾收集器
3.1 Serial/Serial Old收集器
Serial收集器是單線程收集器,在進行垃圾回收時,必須暫停其他所有線程
Serial Old收集器是Serial收集器的老年代版本
Serial收集器和Serial Old收集器對新生代都采用了復制算法,對老年代采用了標記-整理算法,都可以在Client模式下的虛擬機使用,在Server模式下,Serial Old還可以搭配Parallel Scavenge ,在並發收集發生Concurrent Mode Failure時作為CMS收集器的備用方案使用
3.2 ParNew收集器
是Serial收集器的多線程版本,也需要暫停用戶線程,對象分配與回收策略與Serial收集器基本一致,除了Serial收集器,只有它能與CMS搭配使用。
3.3 Parallel Scavenge/Parallel Old收集器
Parallel Scavenge:針對新生代, 多線程, 采用復制算法
Parallel Old: 針對老年代, 多線程,標記-整理算法
吞吐量優先:用戶代碼運行時間/(垃圾收集時間+用戶代碼運行時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%
3.5 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器 ,
應用場合互聯網,B/S服務器端,重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗
采用標記-清除算法,易產生碎片。
處理過程:初始標記,並發標記,重新標記,並發清理
初始標記和重新標記是需要暫停用戶線程
3.6 G1收集器
面向服務器端應用的垃圾回收器,針對老年代和年輕代,采用標記整理算法
具有以下四個特征:
並行與並發:G1能充分利用多CPU、 多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過並發的方式讓Java程序繼續執行。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。 雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、 熬過多次GC的舊對象以獲取更好的收集效果。
空間整合:與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“復制”算法實現的,但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。 這種特性有利於程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。
可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關註點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
處理過程:初始標記,並發標記,最終標記,篩選標記
4.內存分配與回收策略
4.1對象優先在Eden分配
大多數情況下,對象在新生代Eden區中分配。 當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC 。
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。 Major GC的速度一般會比Minor GC慢10倍以上。
4.2大對象直接進入老年代
大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象)
4.3長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡(Age)計數器。 如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1。 對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中
4.4動態對象年齡判定
並不總是要求對象的年齡到達一定程度才進入老年代。如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代
4.5空間分配擔保
Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麽Minor GC可以確保是安全的。 如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。 如果允許,那麽會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
此處冒險是指:新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在MinorGC後仍然存活的情況(最極端的情況就是內存回收後新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。 與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間
該文章大部分參考自:《深入理解Java虛擬機》周誌明 著
JVM-垃圾回收