jvm垃圾回收的流程
允許GC之後,開始查找那些允許被回收的(兩個算法)-> 開始回收(四個算法)
第一步:那些對象是垃圾:
1,引用計數法:通過對引用的遍歷,找到對應的實例,讓對應的實例計數加 1 ,如果引用取消,或者指向null,實例的引用減 1 。把找到的引用都遍歷一遍之後,如果發現有對象實例的計數是0。那麼這個對象 就是垃圾對象了。在通過垃圾回收算法對其進行 回收即可。
缺點:想想一下,有兩個類,互相引用,也就是A對象的實例(也就是對象的全局變量)是一個指向B對象的引用,B對象實例是一個指向A對象的引用。那麼這兩個對象的引用計數,永遠不可能是0 。也就不可能對其進行回收了。
2,可達性分析法:這個算法類似於樹的遍歷,學過數據結構的小夥伴應該會好理解。簡單來說,按照一定的規則說明那些可以作爲一個根節點(GC root),然後以這些根節點去訪問其引用的對象,被訪問的對象又會有其他對象的引用。想象一下,是不是像極了樹的遍歷。這個路徑稱作引用鏈,但凡是在引用鏈上的對象,都是可用的。注意,引用連的起始點都是GC root 哦。雖然有其他對象存在類似於引用鏈的結構,但是,起始點不是GC root的那一些,都是垃圾,可以被回收的。
一般情況下,都是使用的 可達性分析法去查找垃圾類實例。
GC root哪些對象會被認爲是root;
GC root的查找規則:java棧中的引用,方法區中的靜態屬性(靜態變量 + 靜態常量),方法區中常量引用的對象(方法區中有個結構 叫做 常量池 ,存儲的一部分是常量),本地方法(線程獨佔區中有個結構叫做 本地方法棧)。
jvm裏面有一個存儲虛擬s1和s2
年輕代裏面有一個複製算法,這個就要說到
第二步:垃圾回收器算法(標記-清除、複製算法、標記-整理、分代算法)
1,標記-清除:找到垃圾類之後,標記一下。然後直接 清除即可。(算法很快)
缺點:產生空間碎片,不利於大對象的安排進去。
2,複製算法:將內存分爲四塊:新生代(Eden),生存代(Survivor * 2),老年代。有五種內存分配策略,講完之後再說。類的升級流程是Eden->Survivor->老年代;
算法流程:1),先找到垃圾類,將可以使用的類移動到Survivor2,將Eden + 另一塊Survivor1中的內存全部清除。
2),將新生成的類實例優先分配到Eden,分配不下時,放到Survivor2。進行GC時,將Survivor2中對象的滿足一定條件(例如對象年齡達到某一個標準)的對象分配到老年代中。將本次GC存活下來的分配到Survivor1中,在清除Eden + Survivor2 。依次循環即可。
缺點:很容易發現吧,Survivor中每次都會浪費一個Survivor的內存沒有使用,所以爲了減少浪費,一般將Eden的內存擴大,Survivor的內存設置小一點。例如:HotSpot(HotSpot是8中的jvm默認虛擬機) 中設置的是 8 : 1 : 1;
3,標記-整理:看名字是不是感覺很熟悉,沒錯。跟標記-清除很像,也是直接標記。改算法使用到了前面兩個算法的精華,改善了缺點。
算法流程:1),直接標記
2),集中,無縫隙的移動到一端,此時會發現,剩下的垃圾類,都會在其他地方。移動完成之後就會發現有一個邊界,就是可用類跟其他空間的一個邊界,下一步直接把邊界以外的空間直接清除掉就可以了。
缺點:看起來很完美,但是越完美的,往往在時間上過不去。
4,分代算法:根據在哪裏清除,選用算法不一樣。
算法流程:1),新生代採用複製算法
2),老年代採用標記-清除算法(老年代GC很少訪問,類也很少去直接分配到裏面,內存碎片的可怕性就顯得不那麼重要了)
什麼樣的數據會往老年代裏面遷移呢;
每次進行垃圾回收的時候,比如說當前這個對象存活下來了,計數器就會給他+1,默認的話,是當計數器達到16的時候就會放到老年代裏面;
GC的作用域:
JVM內存分配原則:
1,對象優先分配到Eden區域;
2,大對象直接分配到老年區:大對象的就是,對象裏面有很大數組或者很大的字符串;
3,長時間存活的對象存入老年區:就是上面複製算法裏面說的那個對象升級流程;
4,動態對象年齡判定:jvm並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold纔可以進入老年代,如果Survivor空間中年齡相同的所有對象的總空間>=本servivor中的一半,那麼年齡>=本年齡的對象可以直接進入老年區;
5,空間分配原則:簡單來說,就是在發生Minor GC(在新生代進行GC)情況下,爲了防止發生在Minor GC後,Eden有大量存活的對象,導致survivor不能全部存入,這時需要老年代去擔保,把這些對象放入老年代,但是要確保老年要存的下。
1),再發生Minor GC之前,檢查老年區的可用的連續空間是否是大於新生代(Eden)的所有對象的總空間,如果是,直接全部晉升老年代,保證Minor GC的安全;
2),如果不行,就檢查HandlePromotionFailure(可以手工設定)參數時候允許擔保失敗,允許的話,直接分配。不能的話,發生一次full GC(或者是Major GC 在老年代進行GC)。
3),不允許擔保失敗,發生一次 full GC。
爲什麼不直接進行full GC ,因爲速度慢呀。而且經常GC 也 效果不大,因爲老年代都是一些長期存活的對象。
如果老年代內存也不夠用了怎麼辦呢;
他會進行fullGC
fullGC的時候會有什麼現象嗎;有沒有遇到到fullGC的時候影響業務的場景;
如果頻繁的fullGC會出現cpu內存飆升的問題
收集器有:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(G1是目前最好的收集器)
上圖是HotSpot的垃圾收集器的使用範圍,HotSpot是現在主流的 jvm。
CMS
CMS(Concurrent Mark Sweep)一種以獲得最短停頓時間爲目標的收集器,非常適用B/S系統。
使用 Serial Old 整理內存。
CMS 運行過程:
1、初始標記
標記 GC Roots 直接關聯的對象,需要 Stop The World 。
2、併發標記
從 GC Roots 開始對堆進行可達性分析,找出活對象。
3、重新標記
重新標記階段爲了修正併發期間由於用戶進行運作導致的標記變動的那一部分對象的標記記錄。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要 Stop The World 。
4、併發清除
除垃圾對象。
CMS 缺點:
1、對 CPU 資源要求敏感。
CMS 回收器過分依賴於多線程環境,默認情況下,開啓的線程數爲(CPU 的數量 + 3)/ 4,當 CPU 數量少於 4 個時,CMS 對用戶本身的操作的影響將會很大,因爲要分出一半的運算能力去執行回收器線程。
2、CMS無法清除浮動垃圾。
浮動垃圾指的是CMS清除垃圾的時候,還有用戶線程產生新的垃圾,這部分未被標記的垃圾叫做“浮動垃圾”,只能在下次 GC 的時候進行清除。
3、CMS 垃圾回收會產生大量空間碎片。
CMS 使用的是標記-清除算法,所有在垃圾回收的時候回產生大量的空間碎片。
注意:CMS 收集器中,當老生代中的內存使用超過一定的比例時,系統將會進行垃圾回收;當剩餘內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時採用 Serial Old 算法進行清除,此時的性能將會降低。
線程類型: 多線程
使用算法: 標記-清除
指定收集器: -XX:+UseConcMarkSweepGC
G1
G1 GC 這是一種兼顧吞吐量和停頓時間的 GC 實現,是 JDK 9 以後的默認 GC 選項。G1 可以直觀的設定停頓時間的目標,相比於 CMS GC,G1 未必能做到 CMS 在最好情況下的延時停頓,但是最差情況要好很多。
G1 GC 仍然存在着年代的概念,但是其內存結構並不是簡單的條帶式劃分,而是類似棋盤的一個個 region。Region 之間是複製算法,但整體上實際可看作是標記 - 整理(Mark-Compact)算法,可以有效地避免內存碎片,尤其是當 Java 堆非常大的時候,G1 的優勢更加明顯。
G1 吞吐量和停頓表現都非常不錯,並且仍然在不斷地完善,與此同時 CMS 已經在 JDK 9 中被標記爲廢棄(deprecated),所以 G1 GC 值得深入掌握。
G1 運行過程:
1、初始標記
標記 GC Roots 直接關聯的對象,需要 Stop The World 。
2、併發標記
從 GC Roots 開始對堆進行可達性分析,找出活對象。
3、重新標記
重新標記階段爲了修正併發期間由於用戶進行運作導致的標記變動的那一部分對象的標記記錄。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要 Stop The World 。
4、篩選回收
首先對各個 Region 的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。這個階段可以與用戶程序一起併發執行,但是因爲只回收一部分 Region,時間是用戶可控制的。
線程類型: 多線程
使用算法: 複製、標記-整理
指定收集器: -XX:+UseG1GC(JDK 7u4 版本後可用)