我所理解的JVM(六):內存回收

  Java的一大特色就是虛擬機自己擁有垃圾回收機制,用戶在編程過程當中再也不須要考慮垃圾回收的事情。全部的對象的建立都是在堆空間中,垃圾回收主要針對堆空間。html

  Java是面向對象的,程序運行中不斷有對象被建立和使用。絕大部分對象在使用後,就完成了歷史使命,等待GC回收。java

  虛擬機進行垃圾回收的第一步是須要肯定堆空間中哪些對象能夠被回收,第二步是對對象所在的空間進行清理。根據不一樣的算法會考慮在清理完成後進行(壓縮)整理,防止內存碎片。算法

判斷對象是否能夠被回收的依據編程

  1. 引用計數法
    引用計數很差解決對象之間互相引用的場景,主流的虛擬機都沒有采用這種方式定位。
  2. 可達性分析
    可達性分析是指從「GC Roots」的對象做爲起始點,向下搜索全部引用的對象。當一個對象的根引用不是GC Roots的時候,就說明該對象能夠被回收了。

Java虛擬機中,GC Roots中的對象包括下面幾種:安全

  1. 虛擬機棧中的引用;
  2. 方法去中的類靜態變量的引用;
  3. 方法去中常量的引用;
  4. 本地方法棧中的引用

  這裏提到了引用的概念。java 1.2以後增豐富了引用的類別:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)。數據結構

  • 強引用:Object a = new Object() 屬於強引用。
  • 軟引用:用來描述一些有用但並不是必須存在的對象。對應java中的SoftReference類。
  • 弱引用:非必須存在,比軟引用更弱,只要發生GC必然回收。對應java中的WeakReference類。
  • 虛引用:最弱的引用,沒法經過虛引用獲取對象。虛引用的惟一做用實在這個對象被垃圾回收時收到一個系統通知。對應java中的PhantomReference類。

  GC回收對象時,若是對象覆蓋了finalize()方法而且還沒執行,則將該對象進行一次標記,放到一個叫F-Queue中。沒有覆蓋finalize()或者已經執行過了finalize(),則該對象直接回收。虛擬機在售後會啓動單獨的線程在執行F-Queue中的對象的finalize(),目的是防止finalize()過於耗時影響GC時間。若是在finalize()期間該對象從新有了引用,則該對象逃脫GC,不然進行二次標記等待回收。多線程

  採用可達性分析肯定哪些對象能夠被回收的過程,HosPot虛擬機採用三色標記算法,被標記的對象組成的路徑成爲引用鏈。標記以後就是對對象所處空間進行回收。併發

HosPot虛擬機中的垃圾收集的算法性能

  • 標記-清除:速度塊。缺點是內存變成了不連續空間
  • 標記-整理:標記清除後對不連續的空間進行整理,使得空間變得連續。缺點是整理空間很耗時
  • 複製:將內存空間1:1分爲兩部分,只是用其中1部分,標記清除後將剩餘對象轉移到另外一部分。缺點是比較浪費空間。
  • 分代收集:將內存分爲不一樣區域,針對不一樣區域的特色來肯定採用的上述算法中的哪個。
  • 增量收集:在應用進行的同時進行垃圾回收,其實就是併發。

  HosPot將堆空間分爲年輕代(YongGeneration)和年老代(Old/TenuredGeneration)。年輕代又分爲1個Eden區和2個Survivor區,默認是8:1:1。對象在堆空間內的流轉過程.net

  1. 新建對象優先在Eden區。當Eden區空間不足的時候,進行MinorGC,把存活對象放在SurvivorA區,Eden區清空。
  2. 當Eden區再次空間不足時,Eden區和SurivivorA區同時進行MinorGC,把存活對象放在SurvivorB區,Eden區清空。
  3. 重複上述過程
  4. 當MinorGC後Eden區和Survivor區存活的對象空間比另一個Survivor空間還要大時,原Survivor中的對象進入Old區,Eden中的對象進入Survivor區。
  5. 若是MinorGC後Survivor區中存活的對象已經反覆存活超過必定次數了,直接接入Old區。默認15次。
  6. 當Old區也出現空間不足時,進行Major GC,又稱Full GC,對Old區中的對象進行回收。

  因爲處理器有多核多線程和單核單線程之分,用來執行垃圾清除的垃圾收集器也有多種:

  • 年輕代能夠選擇的有: Serial(串行)、ParNew、ParallelScavenge
  • 年老代能夠選擇的有:CMS、Serial Old、Parallel Old
  • 1.7以後能夠選擇G1收集器應用於整個堆空間

幾個概念:

  • 並行(Parallel):多個垃圾收集線程同時執行,用戶線程仍然處於等待狀態
  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行
  • 串行(Serial):單線程執行

  因爲年輕代和年老代的特色,除G1外,年輕代收集器均採用複製算法,年老代收集器均採用標記整理算法。(注:CMS單次是標記清除,必定次數後或者空間不足時觸發(壓縮)整理)

Serial收集器
  單線程執行,STW的時間比較長,通常應用與Client模式
設置參數:   "-XX:+UseSerialGC":添加該參數來顯式的使用串行垃圾收集器;

ParNew收集器
  多線程執行,行爲、特色和Serial收集器同樣。由於除Serial外,目前只有它能與CMS收集器配合工做。
設置參數:
  "-XX:+UseConcMarkSweepGC":指定使用CMS後,會默認使用ParNew做爲新生代收集器;
  "-XX:+UseParNewGC":強制指定使用ParNew;
  "-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同;

Parallel Scavenge收集器
  Parallel Scavenge垃圾收集器由於與吞吐量關係密切,也稱爲吞吐量收集器(Throughput Collector)。CMS、Parallel等收集器的目的是儘量的減小用戶線程等待時間,即便一段時間內發生屢次GC。而Parallel Scavenge收集器的目的是儘量提升虛擬機的吞吐量,儘量少發生GC,哪怕一個GC的時間比較長。通常應用於須要後臺計算的場景,好比批量處理,科學計算等。
設置參數:
  "-XX:MaxGCPauseMillis"控制最大垃圾收集停頓時間,大於0的毫秒數;
  "-XX:GCTimeRatio"設置垃圾收集時間佔總時間的比率,0<n<100的整數;至關於設置吞吐量大小;
  "-XX:+UseAdptiveSizePolicy"開啓這個參數後,JVM會根據當前系統運行狀況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomiscs);

Serial Old收集器:
  Serial收集器的年老代版,主要應用與Client模式。
  Serial Old收集器在Server模式能夠做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

Parallel Old收集器
  ParNew收集器的年老代版。兩者均爲多線程並行。
設置參數: "-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

CMS收集器(Concurrent Mark Sweep)
  併發標記清理(Concurrent Mark Sweep,CMS)收集器也稱爲併發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器;併發收集器,用戶等待時間短,應用於WEB等Server端。標記整理會產生內存碎片。
設置參數:
  "-XX:+UseConcMarkSweepGC":指定使用CMS收集器
  "-XX:CMSInitiatingOccupancyFraction":設置CMS預留內存空間;JDK1.5默認值爲68%;JDK1.6默認值爲92%;
運做過程:

  1. 初始標記(CMS initial mark):只標記GC Roots中直接應用的對象(須要STW)
  2. 併發標記(CMS concurrent mark):找出堆中全部初始標記的對象的間接引用鏈上的對象(與用戶線程併發)
  3. 從新標記(CMS remark):修正併發標記期間由用戶線程引發的那一部分對象的標記記錄(須要STW)
  4. 併發清除(CMS concurrent sweep)

CMS的缺點

  1. 對CPU資源敏感:CMS的默認收集線程數量是=(CPU數量+3)/4,即CPU線程數較少是不推薦使用。
  2. 預留空間問題:須要爲併發清除時用戶線程新產生的垃圾預留一部分空間,致使出發FullGC的閥值比其餘收集器要小。若是CMS預留內存空間沒法知足程序須要,JVM啓用後備預案:臨時啓用Serail Old收集器,而致使另外一次Full GC的產生; 即當老年代的空間使用率達到68%時,會執行一次CMS回收。若是應用程序的內存使用率增加很快,在CMS的執行過程當中,已經出現了內存不足的狀況,此時,CMS回收將會失敗,JVM將啓動老年代串行收集器進行垃圾回收。若是這樣,應用程序將徹底中斷,直到垃圾收集完成,這是,應用程序的停頓時間可能很長。
  3. 產生內存碎片:CMS基於"標記-清除"算法,清除後不進行壓縮操做;當虛擬機沒法找打足夠連續的內存分配大對象時,會提早出發一次FullGC。解決辦法:1,"-XX:+UseCMSCompactAtFullCollection",發生上述場景時不進行FullGC,而是開啓內存碎片的合併整理過程;合併整理過程沒法併發,停頓時間會變長;默認是開啓的。2,"-XX:+CMSFullGCsBeforeCompaction"設置執行多少次不壓縮的Full GC後,來一次壓縮整理;默認是0。
  4. 因爲空間再也不連續,CMS須要使用可用"空閒列表"內存分配方式,這比簡單實用"碰撞指針"分配內存消耗大;

G1收集器
特色:

  1. 並行與併發,目的是充分利用多CPU多核多線程環境下的硬件優點,縮短STW的時間。
  2. 管理整個GC堆(整個堆劃分爲多個大小相等的獨立區域(Region),仍然保留分代概念)
  3. 空間整合:總體上看採用標記-整理算法,從每一個區域看是複製算法,不產生內存碎片
  4. 可預測的停頓:除了追求低停頓外,還會創建可預測的停頓時間模型。開發者能夠明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒;
    應用場景:服務端,有大內存和多處理多核多線程,要求低GC延遲,堆的內存分配數量大 的場景

設置參數
   "-XX:+UseG1GC":指定使用G1收集器;
  "-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45;
  "-XX:MaxGCPauseMillis":爲G1設置暫停時間目標,默認值爲200毫秒;
  "-XX:G1HeapRegionSize":設置每一個Region大小,範圍1MB到32MB;目標是在最小Java堆時能夠擁有約2048個

可預測停頓的原理
  G1收集器之因此能創建可預測的停頓模型,是由於它能夠有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(每次回收所得到的空間大小以及回收所須要的時間),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的由來)之中使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在遊俠時間誒能夠獲取儘量高的手機效率。

一個對象被不一樣區域引用的問題
  因爲有衆多的region區域,當一個對象被其餘區域飲用時,在作可達性判斷肯定對象是否存活的時候,豈不是要掃描整個Java堆?這個問題並不是只有G1有,只是G1有衆多的區域顯得問題比較突出。 解決辦法
  不管是G1仍是其餘分代收集器,JVM都採用RememberedSet來避免全堆掃描。G1中每一個Region都有一個與之對應的RememberedSet,當虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個WriteBarrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象)。若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的RememberedSet中。當進行內存回首時,在GC根節點的枚舉範圍中加入RememberedSet便可保證不對全堆掃描也不會有遺漏。

若是不計算維護RememberedSet的操做,G1收集器的運做過程

  1. 初始標記(initial marking)
  2. 併發標記(concurrent marking)
  3. 最終標記(final marking)
  4. 篩選回收(live data conuting and evacuation)

G1收集器要求堆空間在6G以上才使用

方法區的回收針對類型的卸載。類型的卸載須要知足3個條件:

  1. 該類的類加載器已經被回收;
  2. 該類沒有任何實例對象;
  3. 該類的clss對象沒有被任何地方引用。

  動態代理,動態生成JSP等頻繁自定義ClassLoader生成class字節碼的場景使得虛擬機必須具有類卸載的功能。什麼時候對該空間進行回收由GC判斷。

  Jdk1.8以後字符串常量池位於堆空間單獨一塊,字符串常量池空間也是能夠被回收的,只須要判斷該字符串是否仍有引用就能夠肯定是否能夠回收。什麼時候對該空間進行回收由GC判斷。

用戶線程狀態
  只要發生GC,就會致使Stop The World。當發生Stop The World的時候,虛擬機中的用戶線程是什麼狀態呢?兩種:

1. 安全點(針對處於running狀態的線程)
  由於程序在運行時不是全部的時候停下來都是安全的(好比運算進行到一半,數據值是一個髒數據),安全點是全部線程在」Stop the world」時到達的一個安全的點。因爲堆中的對象龐大,若爲每一個對象都生成OopMap數據結構將佔用大量空間,因此HotSpot只在」安全點「上生成這些數據結構。同時程序並不是在全部位置均可以中止,而是隻能在安全點纔會中止。因此」安全點「還會影響到GC的及時性。」安全點「選取時不能太多,以形成空間上的支出,也不能太少,以讓GC等待較長時間。 對於」安全點「還有另一個須要考慮的問題是,當GC發生時如何讓全部線程都跑到最近的」安全點「,通常都兩種方案。搶先式中斷,搶先式中斷是虛擬機將全部線程停下來,而後一一檢查是否已達安全點,若沒有到達安全點則恢復線程讓其到達最近的」安全點」,此種方式幾乎沒有虛擬機使用。主動式中斷的思路是中斷髮生時,虛擬機在全部的線程上設置一個標誌,線程自行檢查該標誌,而後進入「安全點」。檢查標誌的地方和安全點是重合的。

2. 安全區域(針對處於非running狀態的線程)
  上面沒有解決的問題是當一個線程處於休眠,或未分配CPU時鐘,好比sleep或blocked狀態時,他就沒法走到安全點去掛起本身。對於這種狀況,就須要經過安全區域來解決,安全區域是一個程序不會更改本身引用的區間,在這個區域的任何地方開始GC都是安全的,能夠被認爲是擴展了的安全點。當線程執行到SafeRegion的代碼時,他就會標記本身已經進入Safe Region,此時發生GC,將不會管這些線程。快要離開安全區域的時候他就會去檢查當前的狀態,若是是在GC中,那邊他就會掛起等待能夠離開Safe Region的信號,不然他就能夠繼續運行。

參考資料:

  1. 《深刻理解Java虛擬機》
  2. 解析JDK 7的Garbage-First收集器
  3. 相對全面的GC總結
  4. HotSpot算法及垃圾收集器簡介
  5. JVM三色標記
  6. 7種垃圾收集器主要特色
  7. 說說GC標記階段的一些事
  8. 理解進入safepoint時如何讓Java線程所有阻塞
  9. Java垃圾回收精粹
相關文章
相關標籤/搜索