1. 程式人生 > >垃圾回收機制與內存分配

垃圾回收機制與內存分配

可用 vivo 比較 固定 類結構 缺點 思路 pau 開發人員

摘要

  程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行者出棧和入棧。每個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(盡管在運行期會由JIT編譯器進行一些優化),因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關註的是這部分內存

一、判斷對象是否存活算法

1、引用計數算法

  給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

2、可達性分析算法

  這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用連,當一個對象到GC Roots沒有任何引用鏈相連時(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用。

二、垃圾收集算法

1、標記-清除算法

  技術分享圖片

  如同它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,它的不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後再程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作 。

2、復制算法

  技術分享圖片

  復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活著的對象復制到另一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效,代價是將內存縮小為了原來的一半,代價太高 。

  IBM公司專門研究表明:新生代中的對象98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這裏指老年代)進行分配擔保

3、標記-整理算法

  技術分享圖片

  復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法
根據老年代的特點,有人提出一種“標記-整理”算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都想一段移動,然後直接清理掉端邊界以外的內存 。

4、分代收集算法

  當前商業虛擬機的垃圾收集都采用“分代收集”算法,這種算法並沒有什麽新的思想,知識根據對象存活周期的不同將內存劃分為幾塊,一般是吧java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都會發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成手機,而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。

三、垃圾收集器

1、Serial收集器

技術分享圖片

  這個收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,知道它收集結束 。

  Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇。

2、ParNew收集器

技術分享圖片

  ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSieThreshold等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,
ParNew是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中一個與性能無關但是很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作,CMS作為老年代的收集器,新生代只能選擇ParNew或者Serial收集器中的一個,ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可以使用-XX:+UserParNewGC選項來強制指定它
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至優於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地可以超越S俄日按來收集器。當然,隨著可以使用的CPU的數量增加,它對於GC時系統資源的有效利用還是有好處的,它默認開啟的線程數與CPU的數量相同,在CPU非常多的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數 。

3、Parallel Scavenge收集器

 技術分享圖片

  此收集器是一個新生代收集器,它也是使用復制算法的收集器,又是並行的多線程收集器,看上去和ParNew都一樣。CMS等收集器關註點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量,所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
停頓時間越短就越適合需要與用戶交互的程序、良好的相應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務
Parallel Scavenge收集器提供了兩個傳輸用於精準控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,系統把新生代調小點,收集300MB新生代肯定比收集500MB塊吧,這也導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。所以此收集器也經常稱為“吞吐量優先”收集器。除上述兩個參數之外,還要一個參數-XX:+UseAdaptiveSizePolicy,這是一個開關參數,當這個參數打開之後,就不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略。

4、Serial Old收集器

技術分享圖片

  Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,那麽它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的後備預案,在並發收集發生Concurrent Mode Failure時使用。

5、Parallel Old收集器

技術分享圖片

  Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法,在Jdk1.5以及之前的版本,新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器外別無選擇,由於老年代Serial Old收集器在服務端應用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果。

  直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合,在註重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器 。

6、CSM收集器

技術分享圖片

  CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的相應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。此收集器是基於“標記-清除”算法實現的,整個過程分為4個步驟,包括:
初始標記、並發標記、重新標記、並發清除
其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”,初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,並發標記階段就是進行GC Roots Tracing的過程,而重新標記階段是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的挺短時間一般為比初始標記階段稍長一些,但遠比並發標記的時間短
由於整個過程中耗時最長的並發標記和並發清除過程手機器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起並發執行的

優點:

  並發收集、低停頓
缺點

  1、在並發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低
  2、CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生
  3、由於CMS是一款基於“標記-清除”算法實現的收集器,這種收集結束後會產生大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出項老年代還有很大空間剩余,不得不提前出發一次Full GC,為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數,用於在CMS收集器頂部至要進行FullGC時開啟內存碎片的合並整理過程,內存整理的過程是無法並發的,空間碎片問題沒有了,但停頓時間不得不變長,還有一個參數-XX:CMSFullGCsBeforeCompaction,這個參數用於設置執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(默認值為0,表示每次進入Full GC時都進行碎片整理)

7、G1收集器

技術分享圖片

  G1收集器是當今收集器技術發展的最前沿成果之一,是一款面向服務端應用的垃圾收集器
  G1收集器特點是:
  1、並行與並發
  2、分代收集
  3、空間整合 從整體來看是基於“標記-整理”算法實現,從局部上來看是基於“復制”算法實現的
  4、可預測的停頓,G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關註點,除了追求低停頓外,還能建立可預測的停頓時間模型
  使用G1收集器,java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合
過程:
初始標記、並發標記、最終標記、篩選回收

四、理解GC日誌

五、垃圾收集器參數總結

技術分享圖片

六、內存分配

  新生代、老年代、永久代
  對象的內存分配,往大方向講,就是在堆上分配(但也可能經過JIT編譯後被拆散為標量類型並間接地棧上分配),對象主要分配在新生代的Eden區上,如果啟動了本地線程分配緩沖,將按線程優先在TALB上分配,少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,取決於當前使用的哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置,本節測試時使用Client模式,驗證的是在使用Serial/Serial Old收集器下

(1)對象優先在Eden分配

(2)大對象直接進入老年代

(3)長期存活的對象將進入老年代

  虛擬機給每個對象定義了一個對象年齡(Age)計數器,年齡增加到一定程度將會被晉升到老年代。

(4)動態年齡分配

  為了更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到才能晉升老年代,如果Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代

(5)空間分配擔保

  在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麽Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麽會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許毛線,那這時也要改為進行一次Full GC。
  Minor GC:指發生在新生代的垃圾收集動作,因為java對象大多具備朝生夕滅的特性,所以MinorGC非常頻繁,一般回收速度也比較快
  Full GC/Major GC:指發生在老年代的GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裏就有直接進行 Major GC 的策略選擇過程) 。
MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

七、總結

  內存回收與垃圾收集器在很多時候都是影響系統性能、並發能力的主要因素之一,虛擬機之所以提供多種不同的收集器以及提供大量的調節參數,是因為只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最高的性能。沒有固定收集器、參數組合,也沒有最優的調優方式,虛擬機也就沒有什麽必然的內存回收行為

  本片文章是參考《深入理解JVM》而寫的,博主推薦大家可以詳細地看一下這本書,裏面講了好多關於JVM深入的知識,可以讓我們這些java開發人員更加地熟悉自己編寫的程序。以後編寫出更加完善的代碼!!!

垃圾回收機制與內存分配