JVM之垃圾收集器

Java的垃圾收集器是區別於C++語言的一個重要特徵。在C++裏面,內存的分配以及回收,都是程序員可控的,這帶來的好處就是,只要你處理得當,內存空間就不會存在大量浪費,但同時,這也是C++程序員最痛苦的地方,每一個內存的分配和回收都需要自己去處理,稍不注意就是刪庫跑路(手動滑稽);而針對這一點,Java就完全將內存的分配以及回收交給了JVM,Java程序員只需要在適當的地方創建對象,分配內存;在對象生命週期結束後,對象的回收不需要程序員親自處理,JVM自己會在特定時間去進行回收處理。而JVM針對不必存活的對象的回收就是依賴垃圾收集器(GC收集器)。

目錄

一、判斷對象所在內存是否需要回收

1、引用計數算法

2、可達性分析算法

二、垃圾收集算法

1、標記-清除

2、複製算法

3、標記-整理

4、分代收集算法

三、安全點

四、收集器

1、Serial收集器

2、ParNew收集器

3、Paranllel Scavenge收集器

4、Serial Old以及Parallel Old收集器

5、CMS收集器

6、G1收集器

五、內存分配

1、對象優先在Eden分配

2、大對象直接進入老年代

3、長期存活的對象進入老年代

4、動態對象年齡判定

5、空間分配擔保


一、判斷對象所在內存是否需要回收

在GC收集器回收整理內存時,肯定不是想回收哪塊就回收哪塊,它針對的內存一般是你不再使用的對象(其實GC收集器不止回收堆上的內存,還會回收方法區、棧裏面的內存,只是相比之下,堆內存的回收的成果要好很多,所以筆者這裏總結的都是以堆爲例)。

1、引用計數算法

判斷對象是否應該存活,一種算法就是通過判斷,這個對象是否被其他地方引用過,如果被引用了,那麼它就應該存活下去,GC回收器就不會回收它的內存。而引用計數算法就是,當一個對象被另一個地方引用時,它的引用計數器就會+1,如果失去一個引用就會-1,當該值爲0時,就表明它沒有被引用。

雖然該算法實現簡單,判定效率也很高,但是,在主流的JVM中並沒有使用該方法管理內存。因爲這會存在一個問題:相互引用(這裏筆者就不貼出代碼和例子了,如需瞭解自行百度或者留言)。

2、可達性分析算法

在主流的JVM中,都是通過可達性分析來判定對象是否存活,是否該被清理的。這個算法的思路就是,通過一些稱爲GC Roots(這是複數形式,說明有很多起始點)的對象作爲起始點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明這些對象不可用,可以被清除。讀者可以把這些路徑看成很多個多叉樹,每一個GC Root都是一個樹的根節點,這些樹的所有節點就是可用的對象,而沒有被任何一棵樹鏈接的對象就是不可用的,不能被訪問到的,那麼這個對象也就沒有存在的必要了。

關於GC Roots,在Java語言中,可作爲GC Roots的對象包括下面幾種:

(1、JVM棧中引用的對象

(2、方法區中類靜態屬性引用的對象

(3、方法區中常量引用的對象

(4、本地方法棧中JNI引用的對象

這裏讀者可能會發現,在JVM的內存模型的5個塊中,除了程序計數器,其他4塊,只有堆區域沒有被包含進去,而堆裏面存在了大量的對象,爲什麼沒有包含進去?其實可以這樣理解JVM堆與棧的關係:堆只負責存儲對象以及對象的數據,它不負責使用,當需要使用對象時,都是在棧裏面通過存入指向這個對象的引用來使用這個對象以及它的數據。所以,如果一個對象,沒有被使用,那麼它在棧裏面就找不到任何指向它的引用。

綜上,就可以從JVM棧中爲根節點開始尋找它引用的對象,這樣,棧中所需要的所有在堆裏面的對象就可以被分析標誌出來,那麼剩下的(如果忽略掉方法區和本地方法棧的話)對象就是不可用的,或者不可訪問到的,就可以被清理掉。

二、垃圾收集算法

1、標記-清除

算法如其名,進行可達性分析之後,能夠標記出需要被清理的對象,然後直接清理。

這個算法有兩個主要的不足:首先是效率不高,其次是,標記-清除,會產生大量不連續的內存碎片,這樣導致以後分配較大對象而找不到足夠的連續內存,就不得不提前觸發另一次垃圾收集動作。

2、複製算法

爲了解決效率問題,該算法將可用內存分爲兩塊,每次只使用其中一塊,當其中一塊內存用完了,就將還存活着的對象複製到另外一塊,然後把已使用過的內存空間一次清理掉。這樣就不需要考慮內存碎片等複雜情況,不過弊端就是會縮小內存空間的一半,因爲另一半並沒有服務於用戶。

3、標記-整理

標記之後,將存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,這樣就不會存在內存碎片。

4、分代收集算法

大家所熟知的新生代、老年代,就是在不同的年代中使用不同的收集算法,以達到高效GC的算法。

三、安全點

所謂安全點,就是在這個節點開始GC可以得到高效率的回收內存(JVM一般情況下只會在達到安全點的時候纔會進行一次GC操作)。這也是爲什麼說Java裏面內存的回收以及GC的啓動是不受外部控制的(你以爲JVM暴露了System.gc()這個方法,你就可以隨心所欲啓動GC回收了嗎?too young too simple。當你調用這個方法的時候,是否執行是看JVM心情的。心情好就照顧下你的情緒,執行一次,心情不好,懶得鳥你,你調任你調,照樣不執行)。

這裏涉及到倆個方案:①搶先式中斷②主動式中斷

所謂搶先式,就是GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上(幾乎不使用這種方法了)。

而主動式就是,GC發生,需要中斷線程時,不直接對線程操作,只是設置一個標誌,各個線程執行時主動輪詢這個標誌,發現中斷標誌爲真時,就自己中斷掛起(輪詢點設置在安全點,那麼中斷時就保證在安全點了)。

安全區域:這個區域中,任意地方GC都是安全的,引用關係不會發生變化(如果發生變化,那麼之前的垃圾收集算法進行的標誌,哪些對象不可訪問,沒有被使用,就可能會出錯)。

四、收集器

1、Serial收集器

在進行GC時,必須暫停其他所有工作線程,直到收集結束 。

新生代中採用複製算法,老年代中採用標記-整理算法。

雖然當它工作時,會暫停其他所有的工作線程,但是因爲它的高效,其實停頓時間是很少的(可控在幾十毫秒到100多毫秒),至少在不頻繁GC的情況下,基本是察覺不到的。

Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇。

2、ParNew收集器

這個收集器相當於是Serial的多線程版,Serial在進行GC時,只會使用一個CPU(即使處理器是多核的,它不管,管你上天入地,它就用一個CPU,一個線程去GC),而ParNew就是使用多條線程進行GC。該收集器算是運行在Server模式下的虛擬機中,首選的新生代收集器,其中一個性能無關的原因就是,除了Serial收集器,它是唯一一個能與CMS收集器(下面會提及)配合工作的。

3、Paranllel Scavenge收集器

新生代收集器,也是使用複製算法的多線程收集器。和ParNew的不同,它的特別主要體現在該收集器關注點和其他收集器不同,其他收集器在乎的是GC時,用戶線程的停頓時間,而它則是達到一個可控制的吞吐量(就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間))。

4、Serial Old以及Parallel Old收集器

分別是Serial以及Parallel Scavenge收集器的老年代版本。Serial Old收集器主要意義在於給Client模式下的JVM使用。Parallel Old,如果新生代選擇Parallel Scavenge收集器,老年代除了Serial Old收集器之外別無選擇。

5、CMS收集器

以獲取最短回收停頓時間爲目標的收集器。大部分集中應用在B/S系統的服務端。

CMS收集器是基於標記-清除算法實現的,運作過程大致分爲4個步驟:

①初始標記②併發標記③重新標記④併發清除(如需瞭解每個步驟什麼內容,請自己百度,手動滑稽)

耗費時間的②和④因爲是和用戶線程併發執行的,所以停頓時間在就會給用戶很好的體驗。

CMS收集器3個明顯缺點:

對CPU資源非常敏感。

無法處理浮動垃圾(因爲GC標記之後,是併發清除,所以在GC時,用戶線程還在不斷製造垃圾,而這些垃圾是標誌之後發生的,那麼就沒有被標記過,GC時自然無法回收這些沒有被標記,新產生的垃圾)。

基於 標記-清除 的算法,那麼不可避免就會產生碎片化的內存空間。

6、G1收集器

特點:

①並行併發②分代收集③空間整合:基於 標記-整理 算法④可預測停頓

G1收集器運作步驟:

①初始標記②併發標記③最終標記④篩選回收

五、內存分配

1、對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC(指發生在新生代的垃圾收集動作,該動作比較頻繁,回收速度也比較快,而Full GC/Major GC指放生在老年代的GC動作,該GC速度一般比Minor GC慢10倍)。如果Minor GC之後,發現新生代內存還是不足以分配給新對象,那麼之前保存在新生代的對象就會提前進入老年代。

2、大對象直接進入老年代

大對象,指需要大量連續內存空間的java對象,比如數組,很長的字符串。JVM有一個參數(PretenureSizeThreshold),可以設置,對象大於這個參數那麼直接進入老年代。

3、長期存活的對象進入老年代

再進行多次GC後,如果一個對象還存活着,那麼它就該進入老年代,一般這個參數MaxTenuringThreshold設置爲15,就是15次GC後還存活的對象(每一次GC之後存活的對象,年齡都會+1)。

4、動態對象年齡判定

如果相同年齡的對象所佔內存空間的總和,大於Survivor空間的一半,那麼該年齡和大於該年齡的對象會被轉移到老年代(Survivor)

5、空間分配擔保

每次發生Minor GC時,都會進行一次判定,如果老年代最大連續可用的空間大於Minor GC的對象內存總和,那麼這一次Minor GC是安全的,如果不大於,則根據參數值HandlePromotionFailure的值,來進行“冒險”(老年代最大可用連續空間是否大於歷次晉升老年代對象的平均大小,如果大於,則進行Minor GC,否則還是發起Full GC)或者Full GC。

關於JVM的垃圾回收機制總結就到這裏,如果讀者有什麼好的建議或者指正的地方懇請留言。

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