Java虛擬機垃圾收集器與內存分配策略

Java虛擬機垃圾收集器與內存分配策略

概述

  • 那些內存需要回收,什麼時候回收,如何回收是GC需要完成的3件事情。
  • 程序計數器,虛擬機棧與本地方法棧這三個區域都是線程私有的,內存的分配與回收都具有確定性,內存隨着方法結束或者線程結束就回收了。
  • java堆與方法區在運行期才知道創建那些對象,這部分內存分配是動態的,本章筆記中分配與回收的內存指的就是:java堆與方法區

判斷對象已經死了

  • 引用計數算法:給對象添加一個引用計數器,每當有一個地方引用它,計數器+1;引用失敗,計數器-1.計數器爲0則改判斷該對象已死。 但是這種方式,很難解決java對象間相互循環引用的問題。所以主流的java虛擬機,都沒有使用該方式判斷對象是否存活。
  • 可達性分析(Reachability Analysis):通過GC Roots作爲起點,以這些節點通過引用鏈(Reference Chain)向下搜索,當GC Roots沒有引用鏈能夠到達某個對象A的時候,A可以認爲是不可用對象。這類對象就可以判定爲可回收對象
1.引用
  • 引用的狹隘定義:當reference類型數據中存儲的數值是另一塊內存的起始地址,就稱這塊內存代表着一個引用。但是這種方式不能描述一些“食之無味,棄之可惜”的對象
  • JKD1.2之後對引用概念做了擴充,對象分爲強引用(Strong Regerence),軟引用(Soft Reference),弱引用(Weak Reference),虛引用(Phantom Reference)
  • (1)強引用指new這類的引用,只要強引用還在,收集器永遠不會回收掉被引用的對象
  • (2)軟引用用來描述一些還有用,但是非必須的對象,在系統要發生內存溢出的時候,會對這類對象二次回收,如果空間還不夠,拋出OOM
  • (3)弱引用的對象只能生存到下一次收集發生之前。無論內存是否足夠。
  • (4)虛引用是最弱的一種引用關係,爲一個對象設置虛引用唯一目的只是在這個對象被回收的時候收到一個系統通知。
2.回收方法區
  • Java虛擬機規範說過可以不要求回收方法區(永久代),在新生代中,垃圾收集一次可以回收70%以上內存。但是在方法區(永久代)的收集效率灰常低。主要收集的是廢棄常量與無用的類。
  • 廢棄常量指常量池存在一個字符串abc.但是當前系統沒有對象引用abc,則abc是可以廢棄的常量,可以被回收
  • 無用的類:(1)該類的對象已經被全部回收.(2)加載該類的ClassLoader已經被回收.(3)該類對應的Class對象沒有在任何地方被引用,而且通過反射訪問。HotSpot提供了 -Xnoclassgc參數控制是否回收無用的類

垃圾收集算法

  • 經典常用的收集算法:標記-清除算法(Mark-Sweep),標記-整理(Mark-Compact),複製算法(Copying),主流虛擬機的”分代收集”

1.標記-清除算法

  • 標記清除是最基礎的收集算法:1.標記出所需要回收的對象。2.標記完成後統一回收掉所有被標記的對象。
  • 標記清楚算法不足:1.效率不高。2.清除之後產生大量不連續的片段,如果這些不連續不夠空間存放之後產生的對象,還會提前觸發另外一次收集動作。降低內存效率。

2.複製算法:

  • 複製算法將內存分爲兩等塊A,B,每次使用一塊內存比如A,當A內存滿了之後,將剩下的存活的對象拷貝到B內存,一次性清除A。優點:實現簡單運行高效,並且不用在考慮類似於標記清除方式留下的內存碎片。 缺點是:將原來的內存只使用一半,成本太高。
  • 當前主流的商業虛擬機大多采用這種方式收集新生代。新生代對象98%都是朝生夕死,所以不必要按照1:1分配新生代內存。闢如hotspot的分配爲:8(Eden):1(from survivor):1(to survivor)。同時使用老年代進行內存分配擔保。

3.標記-整理(Mark-Compact)

  • 複製收集算法在對象存活率較高的時候,會產生大量的複製工作,影響性能。因爲老年代,一般對象比較大,並且存活率高,一般不能使用該算法。
  • 標記-整理算法是根據老年代對象存活率高,對象比較大的特性,提出的。其基本原理與標記清除一致,但是後面步驟不是直接清除,而是讓所有存活的對象向一端移動,然後清除掉端邊界以外的內存。

4.分代收集算法

  • 將虛擬機內存主要分成新生代與老年代,新生代一般使用複製算法,由老年代進行分配擔保。老年代使用標記整理算法進行回收。

HotSpot主要算法實現

  • GC-Root節點找引用鏈,GC-Root節點一般爲常量,靜態變量,本地變量表中數據。當應用的方法很多,找引用鏈這個操作,將會消耗很多時間
  • 可達性分析,對時間的敏感還體現在GC停頓上,分析的時候,要求系統是停止的,不可以出現可達性分析過程中,對象引用關係還在不斷的變化,即“stop the word”。
  • 當前主流虛擬機都使用的準確式GC:在HotSpot實現中,使用一組稱爲OopMap的數據結構來達到這個目的。在類加載完成時候,在JIT(即使編譯器)編譯過程中,在特定的位置記錄下棧中那些位置是引用。這樣GC在掃描的時候可以直接得知這些信息了。
  • 安全點:在OopMap協助下,HotSpot可以快速準確完成GC-Root枚舉,但是OopMap的內容變化指令非常多。如果爲每一條指令都生成對應的OopMap,需要消耗大量額外空間,因此,只有在特定位置(安全點:Safepoint)才能記錄這些OopMap信息。方法調用,循環跳轉,異常跳轉等這些功能的指令纔會產生SafePoint。
  • 安全區(safe region):安全區域是引用關係不會變化的一個代碼片,線程執行到安全區的時候,首先標記自己進入了安全區,這段時間JVM執行GC的時候,不用管這些狀態的線程了;線程出安全區的時候,首先會檢查系統是否已經完成了GC Root的選舉,如果完成了繼續執行,如果沒有完成則等待直到收到可以安全離開的信號爲止。

HotSpot主要的垃圾收集器

分代算法可以將收集器分爲新生代收集器和老年代收集器。1.新生代收集器主要包含:Serial(串行),ParNew(並行),Parallel Scavenge(並行);2.老年代收集器主要包含:Serial Old,Parallel Old,CMS(Concurrent Mark Sweep)。

  • Serial是最早的,單線程的新生代收集器,在收集動作的時候,必須暫停其它所有的工作線程,直到收集結束。該收集器幾乎是單CPU中收集速度最快效率最高的收集器。serial對Client模式下的虛擬機來說依舊是很好的選擇,回收新生代一兩百兆基本在100毫秒以內,這些停頓完全可以接受
  • ParNew是Serial的多線程版本,該收集器是Service模式下的虛擬機首選收集器,主要原因是它可以與主流的老年代收集器CMS配合使用。該收集器使用的線程數默認與CPU內核一致,
  • Parallel Scavenge 收集器是關注點是吞吐量(吞吐量=運行用戶代碼時間/(運行代碼時間+垃圾收集時間)),停頓時間越短,則用戶體驗越好,高吞吐量則可以高效利用CPU時間,儘快完成運行任務。該收集器適合大量後臺運算而不需要太多的交互的系統。該收集器也叫做吞吐量優先收集器。
  • Serial Old 採用的是標記整理算法。和Serial收集器一樣,都是單線程收集器。一般使用於Client運行模式下。
  • Parallel Old是Parallel Scavenge收集器配合使用的老年代收集器,使用的是標記整理算法,如果新生代使用了Paraller Scavenge算法,則老年代只能使用Parallel Old與Serial Old算法。在注重吞吐量系統,優先考慮Paraller Scavenge與Parallel Old算法。

CMS收集器

是當前老年代主流收集器,採用標記清除算法。CMS整個過程主要分爲4個步驟:1.初始標記;2.併發標記;3.重新標記;4.併發清除.其中初始標記,重新標記仍然需要stop the world。

  • 初始標記只是記錄GC Root能夠直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing,重新標記階段則是爲了修正併發標記期間用戶繼續操作變動的那一部分對象的標記記錄。
  • 耗時比較長的併發標記與併發清除步驟,可以與用戶線程一起工作,不需要stop the world。CMS的優點體現在了併發收集,低停頓。
  • CMS缺點1:對CPU資源非常敏感,在低於4核的時候不建議使用該收集器。CMS默認啓動的回收線程數是(CPU數量+3)/4。
  • CMS缺點2:CMS沒有辦法處理浮動垃圾,因爲標記之後,用戶線程還在執行,會產生其它垃圾,留在下次收集。在jdk1.6之後,CMS收集器啓動閾值已經提升至92%,如果老年代剩下的8%不能滿足浮動垃圾的分配,則會出現Concurrent Mode Failure.從而導致Full GC產生。也可以臨時啓用serial old 重新對老年代收集,但是這樣停頓時間會很長。-XX:CMSInitiatingOccupancyFraction屬性設置CMS的啓動閾值,並且可以根據實際適當降低啓動閾值。
  • CMS缺點3: 使用的是標記清除算法,會產生大量碎片空間,如果無法找到足夠大的連續空間分配當前對象,則觸發一次full gc。

G1收集器

G1收集器被認爲是HotSpot jdk1.7重要進化特徵,是一款面向服務端的垃圾收集器。G1具有以下優勢

  • 並行與併發:G1能夠更充分應用多CPU,多核環境的硬件優勢,減少系統停頓時間。G1可以通過並行的方式,讓java程序繼續執行。
  • 分代收集:G1不需要與其他收集器配合,可以獨立管理整個GC堆。它能夠自己採用不同的方式去處理新建的對象和已經熬過多次GC的老對象,以產生更好的收集效果。
  • 空間整合: G1是基於標記整理算法實現的收集器,不會產生大量不連續的碎片空間,這一點比CMS要有很大提高。
  • 可預測的停頓:降低停頓幾乎是所有當前互聯網企業應用的關注點。G1除了追求低停頓外,還建立了可預測停頓時間模型,能讓使用這指定在一個長度爲M的毫秒時間片段內,收集消耗的時間不長於N毫秒。已經有部分實時的垃圾收集器的特徵了。
  • 其他收集器回收範圍都是整個新生代和整個老年代,G1將整個堆分成多個大小相等的獨立區域(Region),新生代與老年代都是Region的不需要聯繫的集合。回收的時候,有一個優先列表,優先回收價值最大的Region,保證了G1收集器在有限時間內獲取儘可能高的收集效率。
  • Region:每一個Region都有一個對應的Remembered Set。每一次操作Reference時候如果檢查到有引用對象在別的Region中。則記錄Remembered Set。將Remembered Set作爲一個GC-root。可以保證不會對全堆做掃描,也不會有回收遺漏。
  • G1收集器如果不計算維護Remembered Set操作,可以分爲以下步驟:1.初始標記;2.併發標記;3.最終標記;4.篩選回收

內存分配策略

  • 對象優先在Eden分配
  • 大對象直接進入老年代:代碼中勁量避免短命的大對象。-XX:PretenureSizeThreshold=3145728 表示大於3M的對象直接在老年代分配。
  • 長期存活的進入老年代:虛擬機個每個對象一個age計數器,對象每經歷一次Minor GC存活下來,age+1.達到一定年齡值(默認15),則進入老年代。-XX:MaxTenuringThreshold=1表示每次Minor GC之後存活的對象就要進入老年代。
  • 動態判斷對象年齡:如果在survivor中相同年齡對象的大小總和大於survivor空間的一半,年齡大於或者等於該年齡的對象直接進入老年代。
  • 空間分配擔保:創建對象新生代如果沒有足夠空間時候,則由老年代分配內存,這就要求老年代有連續的能夠存放該對象的空間,否則可能擔保失敗,從而觸發Full GC

小結

該內容主要介紹了幾種垃圾收集算法,幾種主流虛擬機收集器以及內存分配的主要策略。根據具體場景選擇關注停頓點還是關注吞吐量,串行還是並行收集器。是調優的重要部分。同時理解內存分配,對於寫java代碼也有一定的提高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章