再看JVM:垃圾回收那些事

前言

JVM虛擬機爲使用者提供了自動內存管理機制,使的程序員在使用完對象後手動釋放佔用內存的工做中解脫出來。內存的動態分配和回收徹底使得一切都看起來那麼美妙,可是再好的機器也有出問題的時候不是。在項目中須要排查各類內存溢出、內存泄漏問題時,就有必要來了解了解JVM內部對內存回收的那些事了。小白由於要在組內作一次JVM垃圾回收的技術分享,因而又再次研讀了《深刻理解Java虛擬機》一書中垃圾收集相關章節。實在是感受每看一遍,都有不一樣的收穫,本文參考虛擬機神書對GC相關知識加以梳理,同時有的地方談了一些小白本身的理解,有失偏頗,還望指正。java

1、垃圾的肯定

何爲垃圾? 數數JVM運行期的內存結構,也就方法區和堆內存兩塊內存區域是線程共享的,虛擬機棧、程序計數器、本地方法棧都是線程私有的,私有就意味着這部份內存會隨線程的結束而釋放,所以垃圾回收是無須關注線程私有的內存的。反卻是方法區和堆(主要),因爲是線程共享的,每一個線程均可以在這塊區域寫數據。隨着線程的結束,這部份內存就會存在大量無用的數據。這些數據就是咱們常說的垃圾,而這些垃圾佔用的內存,就是垃圾回收的目標內存。堆內存中的垃圾即是無用、或者稱之爲死亡的對象,方法區中的垃圾即是無用的常量和類數據。程序員

1.1 對象的死亡宣告機制

空間緊張的內存世界,對於對象而言實在太爲殘酷,能夠說毫無人道主義。只要你沒什麼用了,那麼很差意思,法官便要宣判你的死亡了,而後交由劊子手行刑。可是殘酷歸殘酷,法官是有原則的,就是它須要科學的機制來準確的斷定你是否無用,由於只有這種原則才能保證法官所在的世界正常運轉。算法

1.1.1 斷定算法

對象是否無用的斷定算法有以下兩種:緩存

引用技術算法:於對象內部維護一計數器,每有一處運用某個對象,該對象的引用計數器便加一,每有一處的引用失效,該對象的引用計數器減一。計數器爲0的對象即是無用的,也就是死亡對象。安全

  • 優勢:實現簡單,斷定簡單
  • 缺點:沒法解決對象互相引用的問題(A的屬性引用B,B的屬性引用A,除此以外,A、B兩個對象毫無用處)

可達性分析算法:選取特定性質的對象做爲根對象(GC Roots),像從樹的根節點往下遍歷同樣,從GC Roots向下遍歷其引用鏈,若存在對象到GC Roots怎麼都不可達(無任何一條調用鏈),那麼這些對象即可被回收。多線程

  • 優勢:斷定準確,不存在對象互相引用的問題
  • 缺點:實現複雜,斷定效率比計數法低

主流的Java虛擬機採用的基本都是可達性分析算法,主要看重即是其不存在對象互相引用沒法回收的問題。該算法中存在一個概念GC Roots,虛擬機會遍歷這些對象的調用鏈來肯定其餘對象是否存活。那便有一個前提,能夠做爲GC Roots的對象必須保證是存活的對象。併發

  • 虛擬機棧中(本地變量表)引用的對象。這些對象隨線程生而存在,隨線程死而被釋放,所以這類對象只要存在,就必定存活。
  • 本地方法棧中引用的對象。緣由同上。
  • 方法區中類的靜態屬性引用的對象。通常不多會進行方法區的內存回收,且類的回收斷定較爲嚴苛,所以這中對象基本都是存活的。另外小白也認爲這類對象中選做GC Roots的應該是以jdk自身的類爲主的。
  • 方法區中常量引用的對象。

1.1.2 對象的引用

斷定對象是否無用,其實歸根究竟是斷定對象的引用是否還存在。引用這個概念是比較java特點的詞語,能夠類比C、C++中的指針去理解。一個引用類型的變量的值,是另外一塊內存的起始地址。更爲java特點的是,1.2以後,對引用(Reference)進行了具體化的擴充,也就是常說的強、軟、弱、虛四種。性能

強引用:就是咱們平常new對象前聲明的引用。好比:Object obj = new Object()。其中obj就是強引用。優化

軟引用(SoftReference):通常用來表示能夠存在但非必須的對象。這類對象在內存充足時是能夠存在的,可是在內存不足即將溢出時,會被回收掉。可以使用SoftReference類實例化,構造參數爲要引用的對象。適用場景小白覺的應該是一些非必要的緩存數據,好比圖片文件的流對象,內存充足時緩存下來,每次使用直接讀流,內存緊張時被回收,下次使用再從原路徑讀取。網站

弱引用(WeakReference):也是描述非必須的對象。但這個引用關係比軟引用更弱,弱引用引用的對象只要發生垃圾回收,便會被回收,可是在發生垃圾回收以前,仍是能夠經過若引用獲取到該對象的。適用場景和軟引用相似。

虛引用(PhantomReference):準確叫幻影引用吧,也就是引用是假的。虛引用和對象的生存週期毫無關係。沒法經過虛引用獲取到對應對象。惟一的做用就是使這個對象在被回收時收到一個系統通知。能夠被實例化,但必須和一個引用隊列關聯使用。虛擬機在回收這個對象的時候便會把該引用添加進引用隊列,程序即可經過監控引用隊列來實如今對象回收前進行一些操做。

1.2 肯定對象真正死亡的過程

虛擬機不會簡單地經過一次可達性分析就斷定某個對象死亡繼而進行回收的。一個對象在肯定要回收時至少已經經歷了兩次斷定標記。這裏說的每一次標記能夠理解爲一次可達性判斷。虛擬機標記對象的過程以下圖(小白根據本身理解的畫的圖,歡迎討論):

JVM標記對象進行回收流程圖
虛擬機對對象進行第一次標記的時候,對不可達的對象進行篩選,判斷是否有必要執行 finalize()方法。若對象沒有覆蓋該方法或已經執行過該方法,JVM會認爲該對象沒有必要執行 finalize()方法。

而有必要的對象,會被放進一個F-Quene隊列,由低優先級的Finalizer線程觸發這個隊列中對象的finalize()方法。稍後,JVM對該隊列中的全部對象進行一次小規模(隊列中)標記。若是有對象在finalize()方法中拯救了本身,也就是在這個方法中創建了存活對象到this(本身)的引用鏈(具體如何拯救能夠百度或去書裏看代碼),這個對象會在此次小規模標記中標記爲可達,不然依舊是不可達。

在第二輪標記開始後,JVM會再次斷定對象,將被兩次及其以上被標記爲不可達的對象內存回收,將拯救了本身的對象移出待回收集合。

注意:

  • 全部重寫過finalize()方法的對象在被回收前纔會被執行finalize()方法,而且只要是同一個對象,這個方法在這個對象的整個生命週期中也只會被執行一次。
  • Finalizer線程觸發F-Quene隊列裏對象的finalize()方法時並不保證該方法執行結束,底層應該是有時間限制,超過這個時間會被強制結束。由於若是某個對象的finalize()方法執行緩慢甚至是發生了死循環,便會使Finalizer線程沒法觸發隊列中其餘對象的finalize()方法。
  • 一個對象只能拯救本身一次,由於每一個對象重寫的finalize()只能被觸發一次。此次若是救活了,下次該對象被回收時便不會進入F-Quene隊列。

1.3 方法區垃圾的斷定

對於方法區,並不強制要求虛擬機實現這部分的垃圾回收。主要是由於收集效率低,即耗時長、回收空間少。

方法區主要回收廢棄常量和無用類。廢棄常量的斷定與堆內存中對象的斷定相同。類是否須要回收是由開發人員決定的,HotSpot虛擬機提供的配置參數爲-Xnoclassgc

類的斷定取決於下面三個因素:

  • 堆中不存在該類的實例;
  • 加載該類的ClassLoader已經被回收;
  • 任何地方都不存在該類對應的java.lang.Class的引用。

2、垃圾的回收

垃圾由誰來回收,又是怎樣回收呢? 虛擬機內部提供了適合不一樣場景下的垃圾收集器來進行垃圾回收,程序員能夠本身設定。這些垃圾收集器在程序運行時就是虛擬機內部的一個線程,須要注意的一點是這個線程是守護線程,它會伴隨着咱們程序(主線程)一塊兒結束。GC線程在回收垃圾時,是根據特定的收集算法取進行垃圾內存釋放的。

2.1 垃圾收集算法

  • 標記-清除算法:如名字通常,先標記內存中須要回收的對象(這裏所說的標記,就是對象被最終斷定死亡的標記過程),標記完成後統一對全部被標記的對象進行回收,釋放其所佔用的內存。
  • 複製算法:須要將內存劃分爲大小相等的兩塊,每次只使用其中一塊。須要回收時將使用的這塊內存中全部存活的對象(也是須要對對象進行斷定的)複製到沒用的那塊內存上,而後將以前使用的那塊內存整塊清理,再改用複製過來的這塊內存。
  • 標記-整理算法:與標記清除算法相似,不一樣的是標記完成後不直接清理,而是先將存活的對象統一貫一端移動,移動完成後直接清理存活對象區域之外的空間。
  • 分代收集算法:依賴於上述算法。主要是根據對象的生命週期將內存劃分爲幾塊,每塊內存採起合適的收集算法。通常來講,JVM把堆內存分爲新生代和老年代。

上面簡單描述了各類算法的基本思想。小白這裏梳理各類算法的優缺點及適應場景以下:

標記-清除算法標記和清除兩個過程效率都不過高,在死亡對象特別多的狀況下尤其突出。另外收集完成後會形成內存碎片化嚴重,回收的空間不連續。這兩個特色決定了該算法適合在對象存活週期特別長的狀況下使用,由於這種狀況下每次收集時死亡對象小,在清理時對特定空間的清理就會變少。
複製算法:很明顯的缺點是浪費一半內存,但其簡單高效,且回收後內存連續的優勢也很突出。該算法中回收時是清理使用的內存半區,而後切換複製後的內存半區來使用,相比標記-清理算法確定實現簡單,運行高效。可是須要注意的是,在對象存活較多的狀況下,對應的複製操做就會越多,效率就會越低。所以,複製算法適合在對象存活週期較短的狀況使用
標記-整理算法:很好的彌補了標記-清理算法的缺點,回收後空間連續,無內存碎片化問題。效率上小白感受大多數狀況下是比標記-清理算法略微差一些的,這個沒有深刻研究,只是推測,自己多了一個移動的步驟,若是效率也好的話,那標記-清除算法就沒有必要存在了。也適用於對象存活週期特別長的狀況
分代收集算法:集百家之長,通常是首選堆內存被分爲新生代和老年代新生代對象存活週期短,大都朝生夕死,採用複製算法。HotSpot虛擬機默認按8:1:1的比例將新生代分爲Eden區域和兩塊同樣大的Survivor區域,每次使用Eden和一塊S區,回收時將存活的對象複製到另外一塊S區,回收完成後再使用這塊S區和Eden區。這樣每次只會閒置10%的新生代空間,對於得到了高效率的結果來講這個代價還能夠接受。老年代通常存放存活週期長的對象,每次收集對象存活率高,只能使用標記-清除(整理)算法。注意:新生代中,若收集時存活對象預留的那塊S區放不下時,會依賴老年代存放,具體的機制下面會提到。

2.2 回收的執行者-垃圾收集器

上面提到了HotSpot虛擬機對堆內存的劃分以及收集算法的選用,這裏簡單梳理下收集算法在新生代和老年代具體實現,也就是各個區域的垃圾收集器。

新生代收集器:Serial、ParNew、Parallel Scavenge、G1
老年代收集器:Serial Old、CMS、Parallel Old、G1

搭配組合使用於整個堆內存的回收,可搭配的方式如圖:

各收集器的工做原理這裏不羅列了,感興趣的朋友看下書就知道了,小白只梳理各自的優缺點及適用場景:

  • Serial:新生代收集器、單線程,適用於單CPU單核環境,需設置合適停頓時間
  • ParNew:新生代收集器、多線程,默認開啓收集線程數和CPU數目相同,適用於多核多CPU場景
  • Parallel Scavenge:新生代收集器、多線程、與用戶線程並行、可設置自適應調節(JVM自調優)、關注點是吞吐量(用戶程序運行時間與其加上垃圾回收時間和的比值)、適合在後臺運算,不適合存在太多交互的場景
  • Serial Old:老年代收集器、單線程,搭配合適的新生代收集器以及CMS收集器發生問題時的備案
  • Parallel Old:老年代收集器、多線程、適合注重推圖量以及CPU資源敏感的場合
  • CMS:老年代收集器、併發收集、低停頓,沒法處理浮動垃圾、使用Serial Old做備案,基於標記-清除算法,適用互聯網站或者B/S系統的服務端
  • G1:JDK1.7及之後可用,並行併發、可獨立進行分代收集、空間整合、可預測的低停頓,主要用來取代CMS

須要注意的一點是,上面提到的並行是指GC和應用程序線程並行,併發則指的是多線程回收。

2.3 GC線程工做機制(HotSpot)

HotSpot虛擬機中GC線程在開始工做時是須要掛起應用程序的全部線程以保證回收操做的準確性的,準確說是保證選擇的GC Roots對象和程序當前上下文的一致性。小白畫了流程圖以下,來更形象地描述GC如何中止工做線程。

HotSpot虛擬機GC線程工做機制
圖裏引入了兩個概念,這裏簡單說一下。安全點是在程序運行的特定位置,記錄了該位置的指令執行時內存中可做爲GC Roots的引用的內存地址,方便虛擬機直接去具體位置枚舉根節點,而不是在整個內存中查找。設置安全點也是避免虛擬機爲每條指令都記錄引用信息浪費太多空間。安全域是指該區域內的指令不會致使當前內存中的引用發生變化,也就是說線程在安全域執行不會影響GC的準確性。安全域解決了處於某種狀態(好比Sleep或是Blocked)線程沒法響應JVM中斷要求的問題。

3、垃圾什麼時候回收

瞭解了什麼是垃圾以及如何回收,接下來就簡單聊聊虛擬機何時會進行垃圾回收(不會去詳細說明內存如何分配以及各類虛擬機參數)。首先須要明確的是,進行垃圾回收會發生STW問題,沒法避免,所謂的並行也只是總體看上去是並行的,那麼就意味着頻繁的垃圾回收會極爲影響應用程序的性能,所以垃圾的回收只能發生在必要的時候,也就是可用內存不足覺得對象分配的時候。

HotSpot虛擬機將堆內存劃分爲新生代和老年代新生代又劃分爲三塊,一塊較大的Eden空間兩塊較小的Survivor空間,默認比例爲8:1:1。劃分的目的是由於HotSpot採用複製算法來回收新生代,設置這個比例是爲了充分利用內存空間,減小浪費。新生成的對象在Eden區分配(大對象除外,大對象直接進入老年代,大小的判別閾值可配置),當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。GC開始時,對象只會存在於Eden區和From Survivor區,To Survivor區是空的(做爲保留區域)。GC進行時,Eden區中全部存活的對象都會被複制到To Survivor區,而在From Survivor區中,仍存活的對象會根據它們的年齡值決定去向,年齡值達到年齡閥值(默認爲15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1,GC分代年齡存儲在對象的header中)的對象會被移到老年代中,沒有達到閥值的對象會被複制到To Survivor區。而後清空Eden區和From Survivor區,新生代中存活的對象都在To Survivor區。接着, From Survivor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的From Survivor區,新的From Survivor區就是上次GC的To Survivor區,總之,無論怎樣都會保證To Survivor區在一輪GC後是空的。GC時當To Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象時,須要依賴老年代進行分配擔保,將這些對象存放在老年代中。另外有個特殊狀況是,在Minor GC後,若是S區有相同年齡的存活對象,且相同年齡的對象佔用空間超過了S區的50%,這些對象也會被提早放入老年代。

當有對象放進老年代而最終內存不足時,老年代纔會進行Major GC,其常常伴隨至少一次的Minor GC。老年代的GC通常比新生代的GC慢10倍以上。所以通常來講要儘可能減小虛擬機進行老年代GC。

HotSpot提供的優化措施是分配擔保機制,可經過HandlePromotionFailure參數設置是否容許擔保失敗。通常在進行Minor GC前,這次GC後存活的對象有多少是沒法預知的,最壞的狀況就是全部對象都存活,那麼一塊Survivor區域是絕對放不下,這個時候就須要把存活的對象提早放入老年代。可是老年代也沒法保證能放下啊,因此絕對安全的狀況就是老年代的最大可用的連續空間(不肯定)大於新生代全部對象總空間。分配擔保機制就是在非絕對安全的狀況下,檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,擔保這次Minor GC安全(有風險),若是小於(擔保失敗)直接Full GC。另外,若是設置不容許擔保失敗(其實就是關閉擔保機制)就意味着每次新生代空間不足都會Full GC。

注意:Full GC到底是哪裏的GC衆說紛紜,小白這裏認爲其並不僅僅指老年代GC,而是一次整個堆內存及永久帶的GC。可是在去永久帶後,也就只是整個堆內存的GC了。

總結

JVM的垃圾回收必定要搞清楚的是回收什麼、如何回收、什麼時候回收這三個問題。小白寫這篇文章的時候原本也是按這個思路去嘗試表達本身的理解的,沒想到會寫這麼多。只是寫的過程當中考慮到一些東西的重要性就仍是寫進來了,最後卻感受質量太差,被書中的知識點佔了太多內容,但願各位朋友諒解,權當複習了。本篇文章主要是梳理《深刻理解Java虛擬機——JVM高級特性與最佳實踐》一書中垃圾回收章節的知識點,談談小白本身的理解,如有疑惑的地方歡迎留言探討。

相關文章
相關標籤/搜索