概述
使用Java的同窗都知道在Java中垃圾自動回收,可是就算如此,咱們也得知道Java是如何實現垃圾自動回收,本文咱們就來學習JVM的垃圾收集和內存分配。java
名詞解釋
在瞭解回收器以前,咱們先來了解下幾個名詞
算法
- 吞吐量-----指CPU用於運行用戶代碼的時間和CPU運行時間的總值的比值,好比虛擬機總共運行了100分鐘,用戶代碼運行了99分鐘,垃圾回收時間1分鐘,則吞吐量就是99%。
- 停頓時間-----指回收器正在運行,用戶程序卻在暫停的時間。對於獨佔的回收器而言,停頓時間可能會比較長,使用併發回收器,因爲垃圾回收線程和用戶線程交替運行,程序的停頓時間會很短,可是因爲其效率極可能不如獨佔垃圾回收器(因爲線程上下文的切換須要耗費CPU資源),故系統的吞吐量可能會較低
- Minor GC-----指發生在新生代的垃圾回收動做,由於Java對象大多都具有朝生夕死的特性,因此Minor GC一般很頻繁,通常回收速度也比較快
- Major GC-----指發生在老年代的垃圾回收動做,出現Major GC常常伴隨至少一次的Minor GC。Major GC通常會比Minor GC慢10倍以上。
- 串行-----單線程進行垃圾回收工做,但此時用戶線程仍處於等待狀態
- 併發-----指用戶線程和垃圾回收線程交替執行
- 並行-----指多條垃圾收集線程並行工做,但此時用戶線程仍處於等待狀態
如何判斷對象已死?
Java堆中存放着Java世界中全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象有哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑使用)。數組
引用計數法
- 給對象添加一個引用計數器,每有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。
- 可是引用計數法有一個致命的缺點,若是在一個對象A中引用了對象B,而對象B中又引用了對象A,這就出現了循環引用的問題,雖然兩個對象都已經能夠進行清除了,可是因爲他們互相引用着,因此他們的引用計數器都不爲0,都沒法被回收
根搜索法
- 經過一系列名爲「GC Root」的對象爲起始點,從這些個節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Root沒有任何引用鏈相連時,則證實此對象是不可用的
- 如下幾種對象能夠看成GC Roots:虛擬機棧中的引用對象;方法區中的類靜態屬性引用的對象;方法區中的常量引用的對象;本地方法棧中Native方法引用的對象(其實都是垃圾回收不會做用的區域的對象)
引用類型
- 強引用:指在程序代碼中廣泛存在的,相似於「Object obj = new Object()」這類的引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象
- 軟引用:用來描述一些還有用,但並不是必需的對象。對於軟引用關聯着的對象,只有在系統沒有足夠的內存時,垃圾回收纔會將其回收,不然不會回收。
- 弱引用:也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,只要垃圾回收器就行工做,無論當前內存是否足夠,都會回收只被弱引用關聯的對象
- 虛引用:最弱的一種引用關係,徹底不會對對象的生存時間構成影響,只是爲了在這個對象被回收的時候收到一個系統通知
對象的自救
- 在根搜索算法中不可達的對象,也並不是是「非死不可」的,若是該對象覆蓋了finalize()方法且沒執行過的話,能夠在finalize()中實現自救,若是該方法已經執行過了,那都會被認爲這個對象是「死」的了。
- (方法區的垃圾回收)判斷一個類是不是「無用的類」的三個決定性條件:該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例;加載該類的ClassLoader已經被回收;該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
垃圾收集算法
垃圾收集算法是垃圾回收的核心,關係到垃圾收集的效果和效率bash
標記-清除算法
標記-清除算法從根集合進行掃描,並對存活的對象進行標記。標記完成以後,再掃描整個空間中未被標記的對象進行直接回收,以下圖所示: 服務器
該算法主要有兩個缺點:
- 效率問題,標記和清除過程的效率都不高
- 空間問題,標記-清除以後會產生大量不連續的內存碎片
複製算法
複製算法將內存劃分爲兩個區間,使用此算法時,全部動態分配的對象都只能分配在其中一個區間(活動區間),而另外一個區間(空閒區間)則是空閒的。
複製算法一樣從根集合掃描,將存活的對象複製到空閒區間。當掃描完畢活動區間後,會將活動區間一次性所有回收。此時本來的空閒區間變成了活動區間,下次GC的時候又會重複剛纔的操做,以此循環。 多線程
複製算法的特色:
- 優勢是實現簡單,運行高效,存活對象較少的時候,極爲高效
- 缺點是空間利用率低
標記-整理算法
標記-整理算法採用標記-清除算法同樣的方法進行對象的標記,但在回收不存活的對象佔用空間後,會將全部的存活的對象往一端空閒空間移動,並更新對應的指針。 併發
標記-整理算法的特色:
- 由於有一個存活對象壓縮的操做,解決了內存碎片的問題
JVM爲了優化內存的回收,使用了分代回收的方式,對於新生代內存的回收(Minor GC)主要採用複製算法。
而對於老年代內存的回收(Major GC),大多采用標記-整理算法。
複製代碼
分代收集算法
當前商業虛擬機的垃圾收集都採用「分代收集」算法:post
- 將Java堆分爲新生代和老年代,根據各個年代的特色採用最適當的收集算法
- 新生代大多數都是朝生夕死的對象,適合使用複製算法,高效,由於每一次垃圾回收的時候,對象存活率很低
- 老年代通常都是採用標記-清除或者標記-整理算法
垃圾收集器
在目前的主流JVM中,具體由Serial、ParNew、ParallelScavenge、Serial old、Parallel Old、CMS、G1等七種垃圾回收器,下圖中,表示出了不一樣的垃圾回收器適用於不一樣的內存區域以及各個垃圾回收器之間的配合使用關係。 學習
圖中的七種垃圾回收器,分別用於不一樣的分代的垃圾回收:
- 新生代:Serial、ParNew、Parallel Scavenge
- 老年代:Serial old、Parallel old、CMS
- 全堆:G1
Serial收集器 (JVM參數:-XX:UseSerialGC)
- Serial收集器是最基本、歷史最悠久的收集器
- 工做在新生代,因此採用的是複製的垃圾回收算法
- 是一個單線程收集器,「單線程」的意義不只僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,而是說在它進行垃圾收集時,必須暫停其餘全部的工做線程("Stop The World")
- Stop The World意爲着在進行垃圾回收的時刻只能進行垃圾回收,用戶的工做線程會被暫停,直到垃圾回收過程完成,這對於服務器端的JVM來講是不能夠忍受的
- Serial因爲沒有線程的切換去耗費CPU資源和時間,因此它是效率很是高的,在通常的Client端是可使用這個Serial收集器的
- 由上圖可見,單線程採用複製而且暫停全部其餘線程
ParNew收集器 (-XX:UseParNewGC)
- Serial收集器的多線程版本
- 做用於新生代,採用複製回收算法,也得Stop The World
- 根據CPU核數,開啓不一樣的線程數
Parallel Scavenge收集器 (XX:+UseParallelGC)
- 新生代收集器,使用複製回收算法
- 多線程回收器,與ParNew不一樣是更關注程序運行的吞吐量(用戶代碼運行時間佔總運行時間的百分比)
Serial Old收集器 (-XX:+UseSerialGC)
- Serial的老年代版本,一樣仍是單線程收集器
- 採用標記-整理算法,主要也是在Client模式下的虛擬機使用
Parallel Old收集器 (-XX:+UseParallelOldGC)
- Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法
- 一樣考慮吞吐量優先指標,很是適合注重吞吐量和CPU資源敏感的場合
CMS收集器
- 以最短回收停頓時間爲前提的回收器,屬於多線程回收器,採用標記-清除算法
- 相比以前的回收器,CMS回收器的運做過程比較複雜:
- 初始標記-----僅僅是標記GC Root能直接關聯的對象,這個階段很快,但仍需Stop The World
- 併發標記-----進行的是GC Tracing,從GC Root開始對堆進行可達性分析,找出存活對象
- 從新標記-----爲了修正併發期間因爲用戶程序繼續運做致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長,但遠比並發標記的時間短,也須要Stop The World
- 併發清除-----開始併發清除前面標記的能夠回收的對象,垃圾回收的線程與用戶線程併發執行,因此能達到停頓時間短這個目第 固然,CMS回收器可定也會有相應的缺點:
- 採用的是標記-清除算法,會產生內存碎片
- 在併發清除的階段,用戶線程也在繼續運做,這個時候所產生的垃圾(浮動垃圾)沒法在此次的回收過程當中回收,必須得等到下一次的垃圾回收
- 對CPU資源很是依賴,過度依賴於多線程環境,默認狀況下,開啓的垃圾回收的線程數爲(CPU的數量 + 3)/ 4,當CPU數量少於4個時,CMS對用戶查詢的影響很大
G1收集器
G1是JDK1.7中正式投入使用的用於取代CMS的壓縮回收器。它雖然沒有在物理上隔斷新生代與老生代,可是仍然屬於分代垃圾回收器。G1仍然會區分年輕代與老年代,年輕代依然有Eden區和Survivor區。
G1首先將堆分爲分爲大小相等的Region,避免全區域的垃圾回收。而後追蹤每一個Region垃圾堆積的價值大小,在後臺維護一個優先列表,根據容許的回收時間優先回收價值最大的Region。同時G1採用Remembered Set來存放Region之間的對象引用,從而避免全堆掃描。G1的分區示例以下圖所示: 優化
這種使用Region劃份內存空間以及有優先級的區域回收方式,保證G1回收器在有限的時間內能夠得到儘量高的回收率
G1和CMS運做過程有不少類似之處,整個過程也分爲4個步驟:
- 初始標記-----僅僅是標記GC Root能直接關聯的對象,這個階段很快,但仍需Stop The World
- 併發標記-----進行的是GC Tracing,從GC Root開始對堆進行可達性分析,找出存活對象
- 從新標記-----爲了修正併發期間因爲用戶程序繼續運做致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長,但遠比並發標記的時間短,也須要Stop The World
- 篩選回收-----首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃。這個階段能夠與用戶線程一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升回收效率。
與其餘的GC回收器相比,G1具有以下4個特色:
- 並行與併發-----使用多個CPU(並行)來縮短Stop The World的停頓時間,部分其餘回收器須要停頓用戶線程來進行GC動做,而G1回收器仍能夠經過併發的方式讓用戶線程繼續執行
- 分代回收-----與其餘回收器同樣,分代概念在G1中依然得以保留。雖然G1能夠不須要其餘回收器配合就能獨立管理整個GC堆,但它可以採用不一樣的策略去處理新建立的對象和已經存活一段時間、熬過屢次GC的舊對象,以獲取更好的回收效果。新生代和老年代再也不是 物理隔離,是多個大小相等的獨立Region。
- 空間整合-----與CMS的標記-清理算法不一樣,G1從總體來看是基於標記-整理算法實現的回收器。從局部上來看是基於複製算法實現的;但不管如何,兩種算法都意味着G1運行期間不會產生內存碎片
- 可預測的停頓-----這是G1相對於CMS的另外一大優點,下降停頓時間是G1和CMS共同關注點。G1除了追求低停頓以外,還能創建可預測的停頓時間模型,能讓使用者明確指定一個長度爲M毫秒的時間片斷內,消耗在垃圾回收上的時間不得超過M毫秒
垃圾收集器總結
從前面的介紹咱們能夠直到,在運行使用JVM時,咱們能夠根據不一樣使用環境下選擇不一樣的垃圾回收器(根據設置JVM參數),固然咱們須要直到的是,隨着時間的推移,越日後的垃圾回收器確定會越智能,越好,因此咱們平時使用的通常都是G1垃圾回收器,由於它是一個很是強大的垃圾回收器。
內存配與回收策略
Java技術體系中所提倡的自動內存管理最終能夠歸結爲自動化的解決了兩個問題:
- 給對象分配內存
- 回收分配對象的內存
前面咱們花了很大的篇幅去學習虛擬機中的垃圾收集器體系及其運做原理,如今咱們來看看如何給對象分配內存
對象優先在Eden區分配
大多數狀況下,對象在新生代Eden區分配,當Eden區中沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC
大對象直接進入老年代
所謂的大對象就是指,須要大量連續內存空間的java對象,最典型的就是那種很長的字符串及數組,在給大對象分配內存時應直接將其放至老年代
長期存活的對象將進入老年代
若是對象在Eden區出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1.對象在Survivor區中每熬過一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲,能夠經過參數-XX:MaxTenuringThreshold來設置)時,就會被晉升到老年代中
動態對象年齡斷定
爲了可以更好地適應不一樣程序地內存情況,虛擬機並不老是要求對象地年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡
空間分配擔保
在發生Minor GC時,虛擬機會監測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC。若是小於,則查看HanlePromotionFailure設置是否容許擔保失敗;若是容許,那隻會進行Minor GC;若是不容許,則也要進行一次Full GC
參考資料
周志明--《深刻理解Java虛擬機++JVM高級特性與最佳實踐》和JVM系列(六)-JVM垃圾回收器