本文主要介紹Java垃圾回收(Garbage Collection),90%乾貨,文字頗多,需要耐心一點看。
【對象判斷狀態算法】
------引用計數法
在創建對象時,爲對象創建一個伴生的引用計數器,當有其他對象引用該對象時,將引用計數器的值加1,如果其他對象不再引用該對象就將引用計數器的值減1,所以當引用計數器的值爲0時,就代表不再有任何對象引用該對象,就說明該對象已經"死亡",就可以被判定爲待回收。
但是這樣做是有問題的,在某些情況下,比如:兩個對象相互引用時,這兩個對象就永遠不會被回收。
python採用的是引用計數爲主,分代回收和 標記清除 兩種機制爲輔的策略。
------可達性分析算法
通過一系列的稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
在java語言中,可作爲GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象;
- 方法區中類靜態屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中JNI(即一般所說的Native方法)引用的對象。
【垃圾回收算法】
------標記-清除算法
最早的垃圾回收算法,它是由標記階段和清除階段構成的。
標記階段會給所有的存活對象做上標記,而清除階段會把沒有被標記的死亡對象進行回收。
標記-清除算法執行完成之後會產生大量的不連續內存,這樣當程序需要分配一個大對象時,因爲沒有足夠的連續內存而導致需要提前觸發一次垃圾回收動作。
------標記-複製算法
將內存分爲大小相同的兩塊區域,每次只使用其中的一塊區域,這樣在進行垃圾回收時就可以直接將存活的東西複製到新的內存上,然後再把另一塊內存全部清理掉。
可以解決內存碎片的問題,內存的可用率大幅降低。
------標記-整理算法
標記-整理算法的後一個階段不是直接對內存進行清除,而是把所有存活的對象移動到內存的一端,然後把另一端的所有死亡對象全部清除。
【jvm堆的三個區域】
GC(Garbage Collection):即垃圾回收器。
爲了高效的回收,jvm將堆分爲三個區域
1.新生代(Young Generation)NewSize和MaxNewSize分別可以控制年輕代的初始大小和最大的大小
2.老年代(Old Generation)
3.永久代(Permanent Generation)(保存方法區,已經移除)
在Java8中,元空間(Metaspace)登上舞臺,方法區存在於元空間(Metaspace)。
同時,元空間不再與堆連續,而且是存在於本地內存(Native memory)
堆大小 = 新生代 + 老年代。其中,堆的大小可以通過參數 –Xms、-Xmx 來指定。
默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:
新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小
------新生代
新生代內存按照8:1:1的比例分爲一個Eden區和S0,S1區。(Survivor)
正常情況下,大部分對象是在Eden區中生成的,回收的時候,將Eden區內所有的存活對象保存到S0區內,然後清空Eden區。
當S0區也存滿的時候,則將Eden區和S0區的存活對象保存到S1區,然後清空Eden區和S0區;然後將S0區和S1區互換。(核心思想就是一直保持S1區域爲空)
新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。
新生代垃圾回收的主要目的,就是儘快回收掉那些生命週期比較短的對象。
------老年代
當S1區的空間不足以放下Eden區和S0區的存活對象的時候,就會把對象存放到老年代。
所以,我們可以認爲老年代存放的是一些生命週期很長的對象。
如果老年代也滿了,就會觸發一次Full GC(Major GC + Minor GC),也就是新生代、老年代都進行回收。
Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
出現Full GC經常會伴隨至少一次的Minor GC(不是絕對,Parallel Sacvenge收集器就可以選擇設置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上。
------Metaspace
用於存放靜態文件,如Java類、方法等。
Metaspace包含JVM用於描述應用程序中類和方法的元數據,是由JVM在運行時根據應用程序使用的類來填充的。
【併發垃圾收集/並行垃圾收集】
------並行(Parallel)
指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態;
如ParNew、Parallel Scavenge、Parallel Old;
------併發(Concurrent)
指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行);
用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上;
如CMS、G1(也有並行);
【垃圾收集器】
圖中,基本上涵蓋了主流的垃圾收集器,圖中紫色的線,代表他們可以配合使用。
【Serial收集器】
Serial(串行)垃圾收集器,JDK1.3.1前是HotSpot新生代收集的唯一選擇。
------特點
針對新生代;
採用複製算法;
單線程收集。(會"Stop The World")
------應用場景
HotSpot在Client模式下默認的新生代收集器。
------優勢
簡單高效(與其他收集器的單線程相比);
對於限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;
在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一百多MS),只要不頻繁發生,這是可以接受的
------設置參數
-XX:+UseSerialGC
------Stop TheWorld說明
JVM在後臺自動發起和自動完成的,在用戶不可見的情況下,把用戶正常的工作線程全部停掉,即GC停頓;
【ParNew收集器】
ParNew垃圾收集器是Serial收集器的多線程版本。
------特點
除了多線程外,其餘的行爲、特點和Serial收集器一樣
------應用場景
在Server模式下,ParNew收集器是一個非常重要的收集器,因爲除Serial外,目前只有它能與CMS收集器配合工作;
但在單個CPU環境中,不會比Serail收集器有更好的效果,因爲存在線程交互開銷。
------設置參數
-XX:+UseConcMarkSweepGC |
指定使用CMS後,會默認使用ParNew作爲新生代收集器; |
-XX:+UseParNewGC |
強制指定使用ParNew; |
-XX:ParallelGCThreads |
指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同; |
【Parallel Scavenge收集器】
Parallel Scavenge垃圾收集器因爲與吞吐量關係密切,也稱爲吞吐量收集器(Throughput Collector)。
------特點
新生代收集器;
採用複製算法;
多線程收集;
CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間;而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput)。
------應用場景
高吞吐量爲目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;
應用程序運行在具有多個CPU,對暫停時間沒有特別高的要求的機器上,即程序主要在後臺進行計算,而不需要與用戶進行太多交互;
例如:批量處理、訂單處理、工資支付、科學計算。
------設置參數
-XX:MaxGCPauseMillis |
控制最大垃圾收集停頓時間,大於0的毫秒數; MaxGCPauseMillis設置得稍小,停頓時間可能會縮短,但也可能會使得吞吐量下降; 因爲可能導致垃圾收集發生得更頻繁; |
-XX:GCTimeRatio |
設置垃圾收集時間佔總時間的比率,0<n<100的整數; GCTimeRatio相當於設置吞吐量大小; |
-XX:+UseAdptiveSizePolicy |
JVM會根據當前系統運行情況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略 |
------吞吐量(Throughput)
CPU用於運行用戶代碼的時間與CPU總消耗時間的比值;
即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);
高吞吐量即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;
------垃圾收集器期望的目標(關注點)
停頓時間 |
停頓時間越短就適合需要與用戶交互的程序; 良好的響應速度能提升用戶體驗; |
吞吐量 |
高吞吐量則可以高效率地利用CPU時間,儘快完成運算的任務; 主要適合在後臺計算而不需要太多交互的任務; |
覆蓋區(Footprint) |
在達到前面兩個目標的情況下,儘量減少堆的內存空間; 可以獲得更好的空間局部性; |
【Serial Old收集器】
Serial Old是 Serial收集器的老年代版本
------特點
針對老年代;
採用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);
單線程收集;
------應用場景
主要用於Client模式;
而在Server模式有兩大用途:
在JDK1.5及之前,與Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用
【Parallel Old收集器】
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本
------特點
針對老年代;
採用"標記-整理"算法;
多線程收集
------應用場景
JDK1.6及之後用來代替老年代的Serial Old收集器;
特別是在Server模式,多CPU的情況下;
這樣在注重吞吐量以及CPU資源敏感的場景,就有了Parallel Scavenge加Parallel Old收集器的"給力"應用組合
------設置參數
-XX:+UseParallelOldGC
【CMS收集器】
併發標記清理(Concurrent Mark Sweep,CMS)收集器也稱爲併發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器
------特點
針對老年代;
基於"標記-清除"算法(不進行壓縮操作,產生內存碎片);
以獲取最短回收停頓時間爲目標;
併發收集、低停頓;
需要更多的內存
是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器;第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
------應用場景
與用戶交互較多的場景;
希望系統停頓時間最短,注重服務的響應速度;
以給用戶帶來較好的體驗;
如常見WEB、B/S系統的服務器上的應用
------設置參數
-XX:+UseConcMarkSweepGC
------運作過程
初始標記(CMS initial mark)
僅標記一下GC Roots能直接關聯到的對象;
速度很快;
但需要"Stop The World";
併發標記(CMS concurrent mark)
進行GC Roots Tracing的過程;
剛纔產生的集合中標記出存活對象;
應用程序也在運行;
並不能保證可以標記出所有的存活對象;
重新標記(CMS remark)
爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;
需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
採用多線程並行執行來提升效率;
併發清除(CMS concurrent sweep)
回收所有的垃圾對象;
整個過程中耗時最長的併發標記和併發清除都可以與用戶線程一起工作,所以總體上說,CMS收集器的內存回收過程與用戶線程一起併發執行。
------缺點
對CPU資源非常敏感 |
併發收集雖然不會暫停用戶線程,但因爲佔用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低。 |
無法處理浮動垃圾 |
在併發清除時,用戶線程新產生的垃圾,稱爲浮動垃圾; 這使得併發清除時需要預留一定的內存空間,不能像其他收集器在老年代幾乎填滿再進行收集; 也要可以認爲CMS所需要的空間比其他垃圾收集器大; "-XX:CMSInitiatingOccupancyFraction":設置CMS預留內存空間; |
可能出現"Concurrent Mode Failure"失敗 |
如果CMS預留內存空間無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗; 這時JVM啓用後備預案:臨時啓用Serail Old收集器,而導致另一次Full GC的產生; 這樣的代價是很大的,所以CMSInitiatingOccupancyFraction不能設置得太大。 |
產生大量內存碎片 |
由於CMS基於"標記-清除"算法,清除後不進行壓縮操作; |
總體來看,與Parallel Old垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間;
但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要佔用更大的堆空間;
【G1】
G1(Garbage-First)是JDK7-u4才推出商用的收集器
------特點
並行與併發 |
能充分利用多CPU、多核環境下的硬件優勢; 可以並行來縮短"Stop The World"停頓時間; 也可以併發讓垃圾收集與用戶程序同時進行; |
分代收集,收集範圍包括新生代和老年代 |
能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配; 能夠採用不同方式處理不同時期的對象; 雖然保留分代概念,但Java堆的內存佈局有很大差別; 將整個堆劃分爲多個大小相等的獨立區域(Region); 新生代和老年代不再是物理隔離,它們都是一部分Region(不需要連續)的集合; |
結合多種垃圾收集算法,空間整合,不產生碎片 |
從整體看,是基於標記-整理算法; 從局部(兩個Region間)看,是基於複製算法; 這是一種類似火車算法的實現; |
可預測的停頓:低停頓的同時實現高吞吐量 |
G1除了追求低停頓處,還能建立可預測的停頓時間模型; 可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒; |
------應用場景
面向服務端應用,針對具有大內存、多處理器的機器;
最主要的應用是爲需要低GC延遲,並具有大堆的應用程序提供解決方案;
在下面的情況時,使用G1可能比CMS好:
超過50%的Java堆被活動數據佔用;
對象分配頻率或年代提升頻率變化很大;
GC停頓時間過長(長於0.5至1秒)。
------設置參數
-XX:+UseG1GC |
指定使用G1收集器; |
-XX:InitiatingHeapOccupancyPercent |
當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45; |
-XX:MaxGCPauseMillis |
爲G1設置暫停時間目標,默認值爲200毫秒; |
-XX:G1HeapRegionSize |
設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region; |
------爲什麼G1收集器可以實現可預測的停頓
可以有計劃地避免在Java堆的進行全區域的垃圾收集;
G1跟蹤各個Region獲得其收集價值大小,在後臺維護一個優先列表;
每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);
這就保證了在有限的時間內可以獲取儘可能高的收集效率;
------一個對象被不同區域引用
一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是否需要掃描整個Java堆才能保證準確?
無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全局掃描:
每個Region都有一個對應的Remembered Set;
每次Reference類型數據寫操作時,都會產生一個Write Barrier暫時中斷操作;
然後檢查將要寫入的引用指向的對象是否和該Reference類型數據在不同的Region(其他收集器:檢查老年代對象是否引用了新生代對象);
如果不同,通過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;
當進行垃圾收集時,在GC根節點的枚舉範圍加入Remembered Set;
就可以保證不進行全局掃描,也不會有遺漏。
------運作過程
初始標記(Initial Marking)
僅標記一下GC Roots能直接關聯到的對象;
且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中創建新對象;
需要"Stop The World",但速度很快;
併發標記(Concurrent Marking)
進行GC Roots Tracing的過程;
剛纔產生的集合中標記出存活對象;
耗時較長,但應用程序也在運行;
並不能保證可以標記出所有的存活對象;
最終標記(Final Marking)
爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;
上一階段對象的變化記錄在線程的Remembered Set Log;
這裏把Remembered Set Log合併到Remembered Set中;
需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
採用多線程並行執行來提升效率;
篩選回收(Live Data Counting and Evacuation)
首先排序各個Region的回收價值和成本;
然後根據用戶期望的GC停頓時間來制定回收計劃;
最後按計劃回收一些價值高的Region中垃圾對象;
回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;
可以併發進行,降低停頓時間,並增加吞吐量;
【ZGC】
在 ZGC 收集器中沒有新生代和老生代的概念,它只有一代。
ZGC 收集器採用的着色指針技術,利用指針中多餘的信息位來實現着色標記,並且 ZGC 使用了讀屏障來解決 GC 線程和應用線程可能存在的併發(修改對象狀態的)問題,從而避免了Stop The World(全局停頓),因此使得 GC 的性能大幅提升。
ZGC 的執行流程和 CMS 比較相似,首先是進行 GC Roots 標記,然後再通過指針進行併發着色標記,之後便是對標記爲死亡的對象進行回收(被標記爲橘色的對象),最後是重定位,將 GC 之後存活的對象進行移動,以解決內存碎片的問題。
【參考】
https://www.cnblogs.com/cxxjohnson/p/8625713.html
《深入理解Java虛擬機:JVM高級特性與最佳實踐》
《Thinking in Java》