什麼,你還不知道什麼是JVM垃圾回收?!

一般,咱們在寫java程序的時候,彷佛不多關注內存分配和垃圾回收的問題。由於,這部分工做,JVM已經幫咱們自動實現了。java

這樣看起來,好像很美好,可是任何事情都有兩面性。雖然JVM會自動的進行垃圾回收,可是,若是遇到有些問題,JVM本身也處理不了呢?算法

所以,咱們須要瞭解一下JVM垃圾回收是怎樣運做的,這樣才能在遇到問題的時候,有的放矢。因此,今天就來聊一聊JVM的垃圾回收吧。數組

首先,思考一下,爲何須要進行垃圾回收?安全

咱們知道,在建立對象的時候,Java會把對象的內容放到堆中。隨着時間的推移,堆中的對象確定會愈來愈多,可是,堆的大小是有限制的。若是,咱們不進行垃圾回收,也就是把無用的對象進行清除和回收,那麼JVM將不堪重負,最終致使內存泄漏。微信

既然咱們須要進行垃圾回收,那麼,首先得知道什麼是垃圾。spa

在垃圾收集器對堆內存進行回收前,會先判斷哪些對象還在「存活」,哪些對象已經「死去」(即不可能再被任何途徑使用的對象),這些「死去」的對象,就是咱們須要進行回收的垃圾。.net

那麼,經過什麼方式去斷定是否爲垃圾呢?(即斷定對象是否存活)3d

引用計數算法(已淘汰)對象

引用計數算法,是指給對象中添加一個引用計數器,每當有一個地方引用它時,計數器的值就加1,當引用失效時,計數器的值就減1。當計數器值爲0時,該對象就會被回收。blog

能夠說,引用計數算法的實現很是簡單,斷定效率也很高。可是,咱們忽略了一個問題,在Java中,對象之間是能夠互相循環引用的。若是,兩個對象之間互相循環引用,那麼就會致使,它們之間的引用計數都不爲0(都在等待對方釋放資源),所以,就沒法通知垃圾收集器回收它們。

可達性分析算法

這個算法的思想就是,經過一系列被稱爲「GC Roots」的對象做爲起點,而後向下搜索,所走過的路徑被稱爲引用鏈。當一個對象到 GC Roots之間沒有任何引用鏈時(即從GC Roots到該對象不可達),則證實該對象是不可用的。


這個算法解決了循環引用的問題,只要對象沒法與GC Root之間創建直接或間接的鏈接,就會斷定爲可回收對象。

那麼,什麼對象能夠做爲GC Root呢?通常分爲如下四種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。

  2. 方法區中類靜態屬性引用的對象。

  3. 方法區中常量引用的對象。

  4. 本地方法棧中引用的對象。

既然已經肯定了哪些垃圾能夠被回收,那麼就須要垃圾收集器進行垃圾回收了,咱們來了解一下幾種比較常見的的垃圾收集算法。

標記清除算法


是最基礎的一種收集算法,分爲標記和清除兩個階段。首先,把須要回收的對象標記出來,而後再把他們清除掉。如上圖所示,全部可回收的對象會變成未使用的一片區域。

標記清除算法邏輯清晰,易於操做。可是,咱們能夠看到,未使用的內存區塊都不是連續的,所以,此算法會產生不少的內存碎片。這樣,當一些較大的對象須要分配空間的時候,就找不到足夠的連續內存來存儲,所以會提早觸發GC,同時也浪費了不少的內存空間(內存空間過小,致使不可用)。

複製算法


複製算法,是指把內存區域劃分爲大小相等的兩塊區域。每次只使用其中的一塊,當這一塊內存用完了,就把全部存活的對象複製到另外一塊上面,最後再把已使用過的內存空間一次清理掉。

這樣,就能夠保證內存區域的連續性,不會產生內存碎片,實現簡單,運行高效。可是,這樣的話只有使用原來一半的內存,代價也過高了。

標記整理算法


標記整理算法,標記過程和標記清除算法同樣,可是後續不是進行清除,而是先整理,讓全部存活的對象都向一端移動,而後再清除另外一端的內存區域。

標記整理算法解決了標記清除算法產生內存碎片的問題,同時也解決了複製算法只能利用一半內存的問題,看似是很是的完美。可是,它卻產生了另一個問題。能夠看到圖中,內存的變更很是頻繁,每次整理都有不少存活的對象內存地址發生改變。所以,它的效率會慢不少。

因此,如今通常用分代收集算法。在Java堆中,分爲新生代和老年代,能夠根據各個代的特色,選擇最合適的收集算法。新生代中,每次垃圾收集都有大批對象死去,只有少許對象存活,就能夠選擇複製算法,只須要付出少許存活對象的複製成本便可。而老年代中,對象存活率高,沒有額外空間對它進行分配擔保,所以使用標記清除或者標記整理算法。

堆內存模型

Java堆是內存管理中最大的一塊區域,也是垃圾回收的重點區域。堆分爲新生代、老年代和永久代,新生代又分爲Eden區和Survivor區,Survivor又分爲S0和S1區。在JDK1.8以後把永久代移除了,而用元空間代替。(永久代使用的是堆內存,而元空間直接使用本機物理內存)


新生代中的對象98%都是朝生夕死的,所以把新生代分爲較大的一塊Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor(此處Survivor區也叫From區,另外一塊空的未使用的空間叫To區,From和To區是會交換的,保證空的老是To區)。

當Eden區沒有足夠的空間分配時,會進行一次Minor GC,Eden區大部分對象都被回收,而Eden區和From區存活的對象會放入到To區,而後From和To區進行交換。(若是To區空間不夠,直接進入老年代)

如下幾種狀況會進入老年代。

1) 大對象

大對象就是指須要大量連續內存空間的對象,最典型的就是那種很長的字符串和數組。大對象會直接進入到老年代,這樣作的目的主要是爲了不新生代發生大量的內存複製(大對象的複製成本較高)。

2)長期存活的對象

虛擬機給每一個對象都定義了一個對象年齡計數器。每當進行一次Minor GC,年齡就增長1歲,當年齡超過必定值時(默認是15,能夠經過參數配置),就進入到老年代。

3)動態對象年齡判斷

虛擬機並不要求對象年齡必定要到達15歲才進入到老年代。若是Survivor空間中有某年齡相同的全部對象大小總和大於Survivor空間的一半,則年齡大於等於該年齡的對象就會直接進入老年代。

空間分配擔保

在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是大於,那麼Minor GC能夠確保是安全的(由於,極端狀況下,就算新生代全部對象都存活,也能夠保證安全晉升到老年代)。不然,虛擬機會查看HandlePromotionFailure的值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小。若是大於,將嘗試進行一次Minor GC(儘管有風險);若是小於或者HandlePromotionFailure設置不容許冒險,那麼就先進行一次Full GC。

以上說的有風險,是由於取歷次晉升到老年代對象的平均值這種方式只是經驗值,並不能保證每次都能擔保成功,若是擔保成功還好,若是擔保失敗的話,依然須要進行Full GC。

儘管如此,咱們最好仍是打開HandlePromotionFailure開關,避免過多頻繁的Full GC(由於Full GC的執行速度比Minor GC慢的多)。


本文分享自微信公衆號 - 煙雨星空(mistyskys)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索