JVM總結-垃圾收集器與內存分配策略

垃圾收集器算法

須要回收的對象實例

垃圾收集器在對堆進行回收時,首先要判斷對象是否還存活。數組

判斷對象是否存貨的算法:安全

一、引用計數器(通常JVM都不用這個算法)

給對象添加一個引用計數器,每當一個地方引用他,程序計數器就加一,但引用失效時程序計數器減一,計數器爲0的對象就是再也不使用的對象。多線程

優勢:實現簡單,斷定效率高併發

缺點:很難解決對象之間循環引用的問題(例如兩個對象互相引用,這樣程序技術器永遠不會爲0)框架

二、可達性分析算法

算法的基本思想思路是經過一系列成爲「GC ROOTS」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC ROOTS沒有任何引用鏈相連說明這個對象是不可用的。佈局

JAVA中能夠做爲GC ROOTS的對象性能

  • 虛擬機棧中的引用對象
  • 方法區中的靜態屬性引用對象
  • 方法區中的常量引用對象
  • 本地方法中的JNI引用的對象

斷定爲不可達對象被標記一次並進行篩選,刷選條件爲此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者虛擬機已經調用過這個方法,都視爲沒有必要執行。網站

視爲有必要執行,那麼這個對象會被放到一個F_Queue的隊列中,由虛擬機創建、優先級低的finalizer線程去執行。(並不承諾等待它結束,避免發生死循環或者很慢形成其餘對象等待,致使內存回收系統崩潰)。GC對F_QUEUE隊列中的對象進行第二次標記,若是沒有從新與引用鏈上的對象關聯則被真正回收,不然將被移除「即將回收的集合」。spa

方法區回收:

在方法去中進行垃圾回收性價比比較低。

永久代主要回收兩部分的內容:

  • 廢棄的常量:沒有任何對象引用常量池中的常量,也沒有其餘地方引用這個字面量,常量就會被系統清出常量池。
  • 無用的類:須要知足
  1. 該類的全部實例都被回收
  2. 加載該類的ClassLoader被回收
  3. 該類對應的Class對象沒有在任何地方被訪問,沒法在任何地方經過反射訪問該類的方法

知足這三個條件僅僅是能夠回收,而不是必然要回收,是否必要回收,經過JVM參數進行設置。

在大量使用反射,動態代理,CGLib等字節碼框架,動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都須要JVM具有卸載的功能,以保證永久代不會溢出。

垃圾收集算法

  • 標記清除算法

        思想:首先標記全部須要回收的對象,在標記完成後統一進行回收。

        缺點:

  • 效率低,標記刪除兩個過程的效率都不高
  • 標記清除後產生大量不連續的內存碎片,空間碎片太多會致使後面須要分配較大對象的時候,沒法找到足夠的連續內存而不得提早觸發另外一次垃圾收集動做。 
  • 複製算法

        思想:將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊用完,就將還存活着的對象複製到另外一塊,而後再把已使用過得內存空間一次清理掉。

        優勢:這樣只須要對半個區進行內存回收,特不用考慮內存碎片問題,只要移動堆頂指針,按順序分配內存,實現簡單運行高效

        缺點:將內存縮小了原來的一半

   商業JVM都用這個算法來回收新生代,並不須要按照1:1來劃份內存。而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間。Eden和Survivor的比例爲8:1,當回收時一次性將Eden和Survivor中存活的對象複製到另外一塊Survivor。也就是每次新生代可用內存爲整個新生代內存的90%,只有10%的會被浪費。咱們沒有辦法保證每次回收有不超過10%的對象還存活着。當Survivor內存不夠時須要其餘內存(老年代)作擔保進行分配擔保。在對象存活率較高時就進行了比較多的複製操做,因此老年代不會選擇這個算法。

  • 標記整理算法

        過程和標記清理同樣,只不事後續不是直接對可回收對象進行清理,而是讓全部存活對象向一端移動,直接清除掉端邊界之外的內存。

  • 分代收集算法

    根據對象存活週期的不一樣分爲幾塊,通常把Java堆分爲新生代和老年代。這樣能夠根據各個年代的特色採用適合的算法。新生代中,每次都有大量的對象死去,只有少許存活就採用複製算法,只須要付出少許存活對象複製成本。老年代對象存活率高,沒有額外的空間進行分配擔保使用標記清除或標記整理算法來盡心回收。

垃圾收集器

手機算法是內存回收的方法論,垃圾收集器是內存回收的具體實現。

JVM對垃圾收集器沒有明確的規範,因此各個JVM的垃圾收集器可能有較大的差異。

HotSpot中所包含的垃圾收集器

新生代

  1. Serial收集器

    是一個單線程收集器,不是它只會使用一個CPU或一個縣城去執行垃圾回收的工做。在它執行垃圾回收時必須暫停其餘全部的線程,知道它收集結束。

優勢:簡單高效,對於限定單個cpu的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾回收天然能夠得到更高的單線程收集效率。因此Serial對於運行在Client模式下的虛擬機是一個很好的選擇。

    二、ParNew收集器

    是Serial收集器的多線程版本,包括收集算法、回收策略、STOP THE WORD等。

    除了Serial收集器外只有ParNew可以與CMS收集器配合工做。

    三、Paraller Scavenge收集器

      也是使用了複製算法,又是並行的多線程收集器,可是Paraller Scavenge收集器的目標是達到一個可控的吞吐量(吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),而其餘的關注點則是儘量縮短垃圾收集時用戶線程的停頓時間。停頓時間越短越適合與用戶交互的成率,良好的響應速度可以提升用戶體驗。而高吞吐能夠高效的利用CPU的時間,儘快完成程序的運算任務,主要適合在後臺運算不須要太多交互的任務。

Paraller Scavenge沒法與CMS配合工做

    PS收集器參數

    用於精確控制吞吐量

    -XX:MaxGCPauseMillis:分別是控制最大垃圾收集停頓時間,GC停頓時間是以犧牲吞吐量以及新生代空間換取的所以不是越小越好。

    -XX:GCTimeRadio:直接設置吞吐量大小,是垃圾收集時間佔總時間的比例。默認值爲99,就是容許最大1%的垃圾收集時間。

    -XX:+UseAdaptiveSizePolicy:這是一個開關參數,打開這個參數,就不須要手工指定新生代的大小,Eden和Survivor的比例,晉升老年代對象的年齡。虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整參數以提供最適合的停頓時間或者最大吞吐量。

老年代

四、Serial Old收集器

    Serial Old是老年代版本,也是一個單線程收集器,使用標記整理算法。這個主要是給Client模式下的虛擬機使用。

五、Parallel Old收集器

是Parallel Scanvage的老年代版本,使用多線程和標記整理算法。

在注意吞吐量以及CPU資源敏感的場合,能夠優先考慮使用Parallel Scavenge和Parallel Old。

六、CMS收集器

基於標記清除算法,以獲取最短回收停頓時間爲目標。目前很大一部分JAVA集中在互聯網站或B/S系統的服務端上,這尤爲重視服務響應速度,但願系統停頓時間短。給用戶帶來較好的體驗。

運做過程分爲

初始標記:標記GC ROOTS可以關聯到的對象

併發標記:進行GC ROOTS Tracing 的過程

從新標記:爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的哪一部分的對象標記記錄,這階段的停頓時間比初始標記稍長,但遠低於併發標記。

併發清除:

初始標記和從新標記仍須要STOP THE WORLD,整個過程併發標記以及併發處理過程收集器程序和用戶程序能夠一塊兒工做。整體上講內存回收和用戶線程能夠併發執行。

優勢:併發收集,低停頓

缺點:

一、CMS收集器對CPU資源十分敏感,

二、沒法處理浮動垃圾

三、收集結束會產生大量的空間碎片

GI收集器

是一款面向服務端的垃圾收集器

優勢:

一、並行與併發

二、分代收集

三、空間整合

四、可預測的停頓

使用G1收集器,JAVA堆的內存佈局就與其餘的不一樣。它將JAVA堆劃分爲多個大小相等的獨立區域(Region),雖然仍是分了新生代和老年代,可是新生代和老年代再也不物理隔離,都是一部分不須要連續的Region的集合。

運做大體劃分爲

  • 初始標記:僅僅標記GC Roots能關聯到的對象,而且修改TAMS(Next Top At Mark Start)的值。讓下一階段用戶程序併發運行時,能在正確可用的Region中建立對象,這階段須要停頓線程,可是耗時很短。
  • 併發標記:GC Roots對堆中的對象進行可達性分析,找出存活對象,這部分耗時比較長,可是可與用戶進行併發執行。
  • 最終標記:修正因併發標記期間用戶進程併發執行而致使標記產生變更的那一部分記錄,JVM將這期間對象變化記錄在線程Remembered Set Log中,最終標記須要把RSL中的數據合併到Remembered Set,這階段須要停頓如今,可是可並行執行。
  • 篩選回收:首先對各個Region的回收價值和成本進行排序,根據用戶所須要的GC停頓時間來定製回收計劃。這階段能夠作到與用戶程序併發執行,由於只回收一小部分Region,時間是用戶可控的,並且停頓用戶線程可大幅度提升手機效率。

內存分配與回收策略:

自動內存管理最終歸結爲自動化解決兩個問題:

  • 給對象分配內存
  • 回收分配給對象的內存

一、對象優先在Eden上分配

大多數狀況下,對象分配到新生代的Eden中,當Eden沒有足夠的空間時,虛擬機會發起一次MinorGC。

二、大對象直接進入老年代

大對象是指,須要大量連續的內存空間,典型的大對象就是很長的字符串數組或者Char數組。

常常出現大對象就會致使內存還有很多空間時就要提早觸發垃圾收集來獲取足夠的連續內存空間。

三、長期存活的對象直接進入老年代

虛擬機給每個對象定義了一個對象年齡計數器,對象在Eden出生通過第一次的MinorGC,而且能被Survivor容納,將被移動到Survivor空間,對象年齡設爲1,對象在Survivor區中每熬過一次,年齡就增長1,年齡增長到必定程度(默認15),就會被晉升到老年區中。對象晉升到老年區的閾值,能夠經過參數設置-XX:MaxTenuringThreshold。

四、動態對象年齡判斷

爲了能更好的適應不一樣程序的內存狀況,虛擬機並非要等到年齡必須達到了MaxTeuringThreshlod纔會進入老年代。若是在Survivor空間中相同年齡的對象大小總和大於Surivivor空間的一半,年齡大於或等於該對象的直接能夠進入老年代

五、空間分配擔保

在發生Minor GC以前,JVM會檢查老年代最大的連續空間內存是否大於新生代全部對象總空間。若是條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則JVM會查看HandlePromotiomFailure設置的值是否容許擔保失敗。容許則會檢查老年代連續可用的內存空間是否大於歷次晉升到老年代的對象大小的平均值,若是大於則嘗試進行一次Minor GC,儘管仍是可能有風險。若是小於或者不容許擔保,則改成進行一次Full GC。

當大量對象在Minor GC後還存活着,就須要老年代進行分配擔保,前提是老年代自己有容納這些對象的空間。一共多少對象存活在內存回收之間是不清楚地,只要取之前每一次晉升到老年代對象容量的平均值做爲經驗值,與老年代剩餘的空間進行比較,決定是否執行Full GC讓老年代騰出更多的空間。若是某次Minor GC存活後的對象忽然增長,遠遠高於平均值,只好再進行一次Full GC,避免Full GC執行過於頻繁。

(在JDK6後就再也不使用HandlePromotiomFailure,只要老年代的連續空間大於新生代總大小或者歷次晉升的平均大小就會進行Minor GC,不然進行Full GC)

相關文章
相關標籤/搜索