1. 程式人生 > >JVM垃圾收集器與記憶體分配策略(總結自《深入理解Java虛擬機器》)

JVM垃圾收集器與記憶體分配策略(總結自《深入理解Java虛擬機器》)

1、物件可用性判斷

垃圾收集器在回收物件前,需要判斷哪些物件沒有被廢棄,哪些物件已經廢棄了(即無法通過任何途徑使用的物件)。所以,垃圾收集器需要一種演算法來判定這個物件是否需要回收。

(1)引用計數演算法

引用計數演算法的基本思想是給一個物件新增一個引用計數器,每當有一個地方引用它時,這個計數器的值就加1,引用失效時,計數器就減去1,當引用計數器的值為0時,就代表這個物件不再可用了。

引用計數演算法實現簡單,判斷效率也比較高,但是它存在一個問題就是無法判斷互相引用問題。 比如兩個物件a和b,a.instance = b,b.instance = a,這個時候兩個物件的引用計數器的值都為2,當我們設定 a = null,b = null後,程式已經無法獲取到這個物件了,但是此時這兩個物件的引用計數器的值為1,意味著它無法被垃圾回收。所以,Java虛擬機器沒有采用這種垃圾回收演算法。

(2)可達性分析演算法

很多主流的語言都是通過可達性分析來判斷是否存活。這個演算法的基本思路就是通過一個稱為“GC Root”的物件作為起始點,從這個物件持有的引用開始搜尋,所走過的路徑稱為引用鏈,當一個物件到GC Root不存在任何引用鏈,就說明這個物件不可用。

在Java中,可作為GC Root的物件有: 1、虛擬機器棧幀中的區域性變量表中引用的物件。 2、方法區中類的靜態屬性所持有的變數(即一個類的靜態變數)。 3、方法區中常量引用的物件。 4、本地方法棧中native方法引用的物件。

在一個物件被標記為不可達物件後,垃圾收集器並不會嘗試去回收它,而是將這個物件作一個標記並進行一次篩選,篩選的條件是該物件所屬的類有沒有重寫finalize方法,如果沒有重寫finalize方法或者finalize方法已被呼叫過,那麼不會進行篩選。任何一個物件的finalize方法只能執行一次,即使在finalize方法中嘗試恢復引用。

(3)Java支援的引用型別

Java支援四種引用型別:強引用、軟引用、弱引用和虛引用。

強引用在Java程式碼中普遍存在,比如 Object a = new Object()中的a就是強引用型別,只要有強引用引用這個物件,那麼垃圾收集器不可能回收掉它,即使發生了記憶體溢位。

軟引用用來表示一些有用但是非必要的物件。當JVM管理的記憶體即將溢位時,才會嘗試將僅被軟引用關聯到的物件進行回收。Java提供了java.lang.ref.SoftReference來支援軟引用。

弱引用同樣也是用來描述非必須物件的,無論記憶體是否充裕,僅被弱引用關聯的物件只能生存到下次垃圾收集發生之前。Java提供了java.lang.ref.WeakReference,弱引用在JDK程式碼中普遍存在,比如WeakHashMap,ObjectOutputStream中的序列化快取,ThreadLocalMap中ThreadLocal型別的鍵,採用弱引用的目的大多數是為了防止記憶體洩漏。

虛引用又稱幽靈引用,是最弱的一種引用關係,Java提供了java.lang.ref.PhantomReference來支援虛引用。無論是否有強引用關聯到這個物件,虛引用物件的get方法都是恆返回null的。它的唯一目的就是當這個物件被收集器回收時得到一個系統通知(即加入到引用佇列),所以,在構造PhantomReference必須指定一個ReferenceQueue。

2、垃圾收集演算法

(1)標記-清除演算法

標記-清除演算法的不足點有兩個:一是效率問題,它的標記和清除兩個過程效率不高。另外一個是空間問題,在清除後容易產生很多記憶體碎片,當程式需要分配一個連續的較大的記憶體區域存放物件時,就不得不再次進行垃圾回收。

(2)複製演算法

為了解決效率問題,複製演算法隨即出現了,它將可用記憶體分為大小相等的兩塊,每次只使用一塊記憶體。當這一塊記憶體使用完後,就啟動垃圾回收,將仍然存活的物件複製到另外一塊,然後將原來的那一塊記憶體全部回收。這樣,記憶體碎片的問題解決了。當然,如果採用這種1:1的比例來劃分記憶體的缺陷顯而易見,就是可用的記憶體縮小為原來的一半。 在這裡插入圖片描述 圖片轉自:http://www.bubuko.com/infodetail-2316435.html

現在很多虛擬機器都採用這個演算法來回收新生代。對於一般的Java程式來說絕大部分物件都是"朝生夕死"的,即物件構造後很快就失去了引用。所以無需按照1:1的比例來劃分記憶體,只需要將記憶體劃分為一個較大的Eden空間和兩塊較小的Survivor空間即可,在執行期間每次使用Eden空間和其中一塊Survivor空間。垃圾回收時,將Eden和Survivor空間仍然還存活的物件複製到另外一個Survivor空間上,最後清理掉Eden和剛才使用過的Survivor空間。

Eden空間和Survivor空間都用於儲存新生代,Eden空間存放剛剛構造的物件,而Survivor空間存放那些在Eden區中經歷了一次垃圾回收後但仍舊存活的物件

HotSpot虛擬機器預設採用Eden區和Survivor區8:1的比例來劃分記憶體,這樣只有10%的記憶體會被浪費。如果Survivor空間不夠用,那麼就需要依賴其它記憶體進行分配擔保

(3)標記-整理演算法

根據老年代的特點,有人提出了標記-整理演算法。標記過程和標記-收集演算法一樣,但是後續操作不是對可回收物件直接進行清理,而是讓存活物件都向一端移動,然後清理掉邊界之外的記憶體。

(4)分代收集演算法

很多商用虛擬機器都採用了分代收集演算法,這種演算法只是根據物件的存活週期將記憶體劃分為幾塊。一般劃分為新生代和老年代。在新生代中,採用複製演算法,在老年代中,採用標記-清理演算法或者標記-整理演算法。

3、HotSpot的垃圾收集策略

(1)列舉根節點

可達性的分析必須需要確保一致性:在整個分析過程中,看起來像是被凍結在一個時間點上,如果在物件的引用狀況不斷地變動的情況下進行分析,其準確性無法得到保證。所以,GC在進行過程中必須先暫停所有的Java執行執行緒(稱為Stop the world)。

目前Java虛擬機器使用的都是準確式GC,所謂準確式GC,就是讓虛擬機器知道記憶體中某位置資料的型別什麼。比如當前記憶體位置中的資料究竟是一個整型變數還是一個引用型別。這樣虛擬機器可以很快確定所有引用型別的位置,從而更有針對性的進行列舉根節點操作。當執行系統停頓後,無需檢查所有的執行上下文和全域性引用位置,HotSpot通過一種稱為OopMap的資料結構來達到這種目的,類載入完成後,HotSpot就會把記憶體偏移量計算出來,GC在掃描時就可以直接得知這些資訊。 在OopMap協助下,HotSpot虛擬機器可以快速完成GC Roots列舉。

(2)安全點

如果引用關係變化,或者OopMap變化的指令非常多,如果為每一條指令都生成對應的OopMap會需要大量的空間。實際上HotSpot並沒有為每條指令都生成OopMap,只是在特定位置記錄這些資訊,這個位置成為安全點。程式執行時只有在達到安全點時才會暫停。安全點的選定是以“是否具有讓程式長時間執行的特徵”為標準進行制定的,例如方法呼叫、迴圈跳轉、異常跳轉等。

還有一個問題就是在GC時如何讓所有的執行緒跑在最近的安全點上停下來,HotSpot採用了主動式中斷的策略,其思想是:當GC需要中斷執行緒時,不直接對執行緒進行操作,僅僅是設定一個標記,各個執行緒執行時主動輪詢這個標誌,發現中斷標記為真時就將自己暫停。

(3)安全區域

如果有執行緒處於阻塞或等待狀態,就無法相應JVM中斷請求,對於這種情況就需要安全區域來解決。安全區域是指一段程式碼片段中,引用關係不會發生變化,在這個地方開始GC是安全的。

當執行緒執行到安全區域時,會標記自己進入了安全區域,當JVM需要GC時,就無需理會那些進入安全區域的執行緒了。當執行緒離開安全區域時,會檢查JVM是否完成了根節點列舉,如果完成了那麼執行緒會繼續執行,如果尚未完成那麼會將執行緒暫停直到可以離開安全區域為止。

4、垃圾收集器

GC可以分為兩種型別:Minor GC和Major GC/Full GC。 Minor GC即新生代GC,指發生在新生代的垃圾收集動作,一般回收速度較快。 Major GC/Full GC為老年代GC,指發生在老年代的GC,出現Major GC之前一般發生了至少一次Minor GC,Major GC一般會比Minor GC 慢10倍以上。

HotSpot一共實現了7種垃圾收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器。

其中,針對新生代垃圾回收的收集器為Serial、ParNew和Parallel Scavenge收集器,針對老年代的收集器為Serial Old、Parallel Old和CMS收集器,G1收集器兩者兼具。

這些收集器的搭配關係和適用場景如下: 在這裡插入圖片描述

(1)新生代收集器:Serial

Serial收集器是最基本的、發展歷史最悠久的垃圾收集器,在JDK1.3以前是新生代收集的唯一選擇。 Serial收集器是單執行緒收集器,在它進行垃圾收集時必須暫停所有工作執行緒直到它執行結束,所以將它使用在服務端可能不是一個好的選擇。但是對於桌面應用,一般記憶體佔用不會很大,Serial收集器在管理這些記憶體時的暫停時間可以控制到幾十毫秒,所以對於Client模式下的虛擬機器是一個比較好的選擇。

(2)新生代收集器:ParNew

ParNew收集器可以看成是Serial收集器的多執行緒版本,其收集演算法、物件分配規則、回收策略和Serial收集器一樣。除了Serial收集器,ParNew是唯一一個能和CMS收集器共同使用的垃圾收集器。

(3)新生代收集器:Parallel Scavenge

Parallel Scavenge也是一款使用了複製演算法的收集器,也同樣採用了多執行緒回收,它的特別之處它可以根據實際需求控制吞吐量(吞吐量即實際程式碼執行時間/總執行時間,總執行時間包含了垃圾回收時間)。停頓時間短則可以給使用者帶來更好的體驗,高吞吐量則可以高效率地利用CPU時間,儘快完成運算任務,適合用於後臺運算。

Parallel Scavenge提供了兩個引數控制吞吐量:控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數和直接設定吞吐量大小的-XX:GCTimeRatio。

GC停頓時間縮短是以犧牲吞吐量和新生代空間換取的。

(4)老年代收集器:Serial Old

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,它主要是用於Client模式下的虛擬機器。

(5)老年代收集器:Parallel Old

Parallel收集器就是Parallel Scavenge的老年代版本,使用多執行緒及其標記-整理演算法,同樣可以控制吞吐量。它只能和Parallel Scavenge搭配使用。

(6)老年代收集器:CMS收集器

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,它比較適合基於B/S的Java服務端應用這種注重響應速度的程式。CMS收集器基於標記-清除演算法,它的運作分為四個部分: 在這裡插入圖片描述 1、初始標記:初始標記會標記GC Root能關聯到的物件,速度很快,但是需要Stop the world 2、併發標記:併發標記就是進行GC Tracing的過程 3、重新標記:重新標記階段則是為了修正併發標記期間使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這一部分停頓時間會稍比初始標記階段長,需要Stop the world 4、併發清除:清除標記的物件。

CMS收集器的優點在於:可以併發收集並且低停頓。 CMS收集器有3個較為明顯的缺點: 1、CMS收集器對CPU資源敏感,雖然它在併發階段不會導致收集器停頓,但是因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量降低。 2、CMS無法處理浮動垃圾。浮動垃圾是出現在標記過程之後的廢棄物件。 3、CMS是基於標記-清除演算法的垃圾收集器,在收集結束後會有大量的記憶體碎片,當無法找到足夠大的空間分配大物件時,就會觸發Full GC。

(7)G1收集器

G1收集器是當今收集器最前沿的成果,它是一款面向服務端應用的垃圾收集器,它可以處理新生代和老年代的物件,G1收集器包含以下特點: 1、並行與併發:G1收集器可以充分利用多核CPU,來縮短stop the world的時間 2、分代收集:分代收集概念仍然在G1收集器中得到了保留,但它能夠採用不同的方式去處理新生代和老年代物件。 3、空間整合:G1整體來看是基於標記-整理演算法實現的收集器,從區域性來看是基於複製演算法實現的,所以,G1收集器不會產生記憶體碎片。 4、可預測的停頓:G1相對CMS除了能夠很好地降低停頓時間外,還能建立可預測的停頓時間模型。

G1收集器將整個Java堆劃分為多個大小相等的獨立區域(Region),新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。

G1之所以能夠建立可預測的停頓時間模型,在於它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1追蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先佇列,每次根據允許的收集時間,優先回收價值最大的Region,這就是G1(Garbage-First)收集器名字的來源。這樣有效地保證了G1收集器在有限的時間能夠獲得儘可能高的收集效率。

G1收集器運作可以分為以下四個步驟: 1、初始標記 2、併發標記 3、最終標記 4、篩選回收

在這裡插入圖片描述

5、記憶體分配、回收策略

物件的記憶體分配簡單來說就是在Java堆上分配,物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配,少數情況下也直接會在老年代分配。

(1)物件優先在Eden區分配 大多數情況下物件在新生代Eden區分配,當Eden區沒有多餘的空間時,將發起Minor GC(新生代GC)。

(2)大物件直接進入老年代 所謂大物件是指需要大量連續記憶體空間的Java物件,典型的大物件就是很長的字串和陣列,經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集來獲取足夠的連續記憶體空間,所以應當儘量避免使用大物件。

(3)長期存活的物件進入老年代 虛擬機器採用分代收集的思想管理記憶體,那麼在記憶體回收時必須要區分哪些物件在新生代,哪些物件在老年代。如果物件在Eden區域分配後並經歷一次Minor GC後仍舊存活,並且Survivor空間能夠容納的話,將被移動在Survivor空間中,並且物件年齡加1,每經歷一次Minor GC後仍舊存活,年齡就會增加1,當它的年齡超過一定數值後(預設15),就會變為老年代物件,年齡閾值可以通過引數-XX:MaxTenuringThreshold調整。

為了能夠更好地適應不同程式的記憶體狀況,虛擬機器並不是一定要到達指定的年齡才能晉升為老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間一半,年齡大於或等於該年齡的物件就可以直接進入老年代。

(4)空間分配擔保 在Minor GC前,JVM會檢查老年代最大可用的連續空間是否大於新生代所有物件佔用的總空間,如果這個條件滿足那麼Minor GC就是安全的。如果不滿足並且HandlePromotionFailure引數允許擔保失敗,那麼繼續檢查老年代最大可用的連續空間是否大於晉升到老年代物件的平均大小,如果大於則嘗試進行Minor GC。如果小於或者HandlePromotionFailure設定不允許擔保失敗,那麼進行Full GC。