JVM垃圾收集器詳解
說起垃圾收集(Garbage Collection,GC),大部分人都把這項技術當做Java語言的伴生產(chǎn)物。事實上,GC的歷史遠比Java久遠,1960年誕生于MIT的Lisp是第一門真正使用內存動態(tài)分配和垃圾收集技術的語言。當List還在胚胎時期時,人們就在思考GC需要完成的3件事情:
- 哪些內存需要回收?
- 什么時候回收?
- 如何回收?
一、哪些內存需要回收?
從JVM區(qū)域結構看,可將這些區(qū)域劃分為“靜態(tài)內存”和“動態(tài)內存”兩類。程序計數(shù)器、虛擬機棧、本地方法3個區(qū)域是“靜態(tài)”的,因為這幾個區(qū)域的內存分配和回收都具備確定性,都隨著線程而生,隨著線程而滅。但Java堆和方法區(qū)不一樣,內存分配都存在不確定性,只有在程序處于運行期間才能知道會創(chuàng)建哪些對象,這部分內存和回收都是動態(tài)的,垃圾收集器所關注的是這部分內存。
在堆里面存放著Java世界幾乎所有的對象實例,垃圾回收器在對堆進行回收前,第一件事情就是就是要確定這些對象哪些還"存活"著,哪些已經(jīng)"死去"。那么又怎么確定對象已經(jīng)"死去"呢?
1.引用計數(shù)法:
分配對象時給對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值就加1;當引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器為0的對象就是沒有再被使用了??陀^地說,引用計數(shù)法(Reference Counting)的實現(xiàn)簡單,判斷效率也很高,但是在主流的Java虛擬機里面沒有選用引用計數(shù)法來管理內存,其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。例如:
public class ReferenceCountingGC { public Object instance = null; private byte[] bigsize = new byte[2*1024*1024]; public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
當設置objA = null;objB = null后這兩個對象再無任何引用,實際上這兩個對象已經(jīng)不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數(shù)都不為0,于是引用計數(shù)算法無法通知GC收集器回收它們。如果這個對象特別大,則會造成嚴重的內存泄露。
2.可達性分析算法:
可達性分析(Reachability Analysis)的基本思想是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(也就是GC Roots到這個對象不可達),則證明此對象是不可用的。如下圖所示:
對象Object5、Object6、Object7相互雖然有關聯(lián),但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象。
二、什么時候回收?
虛擬機為了分析GC Roots這項工作必須在一個能確保一致性的快照中進行,這里的“一致性”的意思就是指在整個分析期間整個執(zhí)行系統(tǒng)看起來就像被凍結在某個時間點上——這叫安全點。當然,程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有到達安全點時才能暫停。安全點選址也有規(guī)定的,選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的。這里的長時間執(zhí)行的最明顯特征是指令列復用,例如方法調用、循環(huán)跳轉、異常跳轉等。
虛擬機為了能讓所有線程都“跑”到安全點上停頓下來,設計了兩個方案:搶先式中斷和主動式中斷。其中搶先式中斷是虛擬機發(fā)生GC時,首先把所有線程全部中斷,如果發(fā)生有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。這種方式現(xiàn)在比較用了。而主動式中斷是虛擬機需要GC時僅僅簡單的設置一個標志,各個線程執(zhí)行到安全點時主動去輪詢這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起。
三、如何回收?
3.1 垃圾收集算法:
(1)標記-清除(Mark-Sweep)算法
這是最基礎的算法,就像它名字一樣,算法分為“標記”和“清除”兩個階段:首先標記處所有需要回收的對象(如哪些內存需要回收所描述的對象),對標記完成后統(tǒng)一回收所有被標記的對象,如下圖所示:
缺點:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除后悔產(chǎn)生大量的不連續(xù)的內存碎片,可能會導致后續(xù)無法分配大對象而導致再一次觸發(fā)垃圾收集動作。
(2)復制算法
為了針對標記-清除算法的不足,復制算法將可用內存容量劃分為大小相等的兩塊,每次只使用一塊。當一塊的內存用完了,就將還存活的對象復制到另一塊上面去。然后把已使用過的內存空間一次清理掉,如下圖所示:
缺點:使用內存比原來縮小了一半。
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,有企業(yè)分析的得出其實并不需求將內存按1:1的比例劃分,因為新生代中的對象大部分都是“朝生夕死”的。所以,HotSpot虛擬機默認的Eden和Survivor的大小比例是8:1。一塊Eden和兩塊Survivor,每次使用一塊Eden和一塊Survivor,也就是說只有10%是浪費的。如果另一塊Survivor都無法存放上次垃圾回收的對象時,那這些對象將通過“擔保機制”進入老年代了。
(3)標記-整理(Mark-Compact)算法
復制算法一般是對對象存活率較低的一種回收操作,但對于對象存活率較高的內存區(qū)域(老年代)來說,效果就不是那么理想了,標記-整理算法因此誕生了。標記-整理算法和標記-清除算法差不多,都是一開始對回收對象進行標記,但后續(xù)不是直接對對象清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存,如下圖所示:
(4)分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據(jù)對象存活的生命周期將內存劃分為若干個不同的區(qū)域。一般情況下將堆區(qū)劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據(jù)不同代的特點采取最適合的收集算法。
3.2 垃圾收集器:
(1)七種垃圾收集器:
- Serial(串行GC)-復制
- ParNew(并行GC)-復制
- Parallel Scavenge(并行回收GC)-復制
- Serial Old(MSC)(串行GC)-標記-整理
- CMS(并發(fā)GC)-標記-清除
- Parallel Old(并行GC)--標記-整理
- G1(JDK1.7update14才可以正式商用)
說明:
- 1~3用于年輕代垃圾回收:年輕代的垃圾回收稱為minor GC
- 4~6用于年老代垃圾回收(當然也可以用于方法區(qū)的回收):年老代的垃圾回收稱為full GC
- G1獨立完成"分代垃圾回收"
注意:并行與并發(fā)
- 并行:多條垃圾回收線程同時操作
- 并發(fā):垃圾回收線程與用戶線程一起操作
(2)常用五種組合:
- Serial/Serial Old
- ParNew/Serial Old:與上邊相比,只是比年輕代多了多線程垃圾回收而已
- ParNew/CMS:當下比較高效的組合
- Parallel Scavenge/Parallel Old:自動管理的組合
- G1:最先進的收集器,但是需要JDK1.7update14以上
(2.1)Serial/Serial Old:
特點:
- 年輕代Serial收集器采用單個GC線程實現(xiàn)"復制"算法(包括掃描、復制)
- 年老代Serial Old收集器采用單個GC線程實現(xiàn)"標記-整理"算法
- Serial與Serial Old都會暫停所有用戶線程(即STW)
說明:
STW(stop the world):編譯代碼時為每一個方法注入safepoint(方法中循環(huán)結束的點、方法執(zhí)行結束的點),在暫停應用時,需要等待所有的用戶線程進入safepoint,之后暫停所有線程,然后進行垃圾回收。
適用場合:
- CPU核數(shù)<2,物理內存<2G的機器(簡單來講,單CPU,新生代空間較小且對STW時間要求不高的情況下使用)
- -XX:UseSerialGC:強制使用該GC組合
- -XX:PrintGCApplicationStoppedTime:查看STW時間
- 由于它實現(xiàn)相對簡單,沒有線程相關的額外開銷(主要指線程切換與同步),因此非常適合運行于客戶端PC的小型應用程序,或者桌面應用程序(比如swing編寫的用戶界面程序),以及我們平時的開發(fā)、調試、測試等。
(2.2)ParNew/Serial Old:
說明:
ParNew除了采用多GC線程來實現(xiàn)復制算法以外,其他都與Serial一樣,但是此組合中的Serial Old又是一個單GC線程,所以該組合是一個比較尷尬的組合,在單CPU情況下沒有Serial/Serial Old速度快(因為ParNew多線程需要切換),在多CPU情況下又沒有之后的三種組合快(因為Serial Old是單GC線程),所以使用其實不多。
-XX:ParallelGCThreads:指定ParNew GC線程的數(shù)量,默認與CPU核數(shù)相同,該參數(shù)在于CMS GC組合時,也可能會用到
(2.3)Parallel Scavenge/Parallel Old:
特點:
- 年輕代Parallel Scavenge收集器采用多個GC線程實現(xiàn)"復制"算法(包括掃描、復制)
- 年老代Parallel Old收集器采用多個GC線程實現(xiàn)"標記-整理"算法
- Parallel Scavenge與Parallel Old都會暫停所有用戶線程(即STW)
說明:
- 吞吐量:CPU運行代碼時間/(CPU運行代碼時間+GC時間)
- CMS主要注重STW的縮短(該時間越短,用戶體驗越好,所以主要用于處理很多的交互任務的情況)
- Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,說明CPU利用率越高,所以主要用于處理很多的CPU計算任務而用戶交互任務較少的情況)
參數(shù)設置:
- -XX:+UseParallelOldGC:使用該GC組合
- -XX:GCTimeRatio:直接設置吞吐量大小,假設設為19,則允許的最大GC時間占總時間的1/(1 +19),默認值為99,即1/(1+99)
- -XX:MaxGCPauseMillis:最大GC停頓時間,該參數(shù)并非越小越好
- -XX:+UseAdaptiveSizePolicy:開啟該參數(shù),-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold這些參數(shù)就不起作用了,虛擬機會自動收集監(jiān)控信息,動態(tài)調整這些參數(shù)以提供最合適的的停頓時間或者最大的吞吐量(GC自適應調節(jié)策略),而我們需要設置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio兩個參數(shù)就好(當然-Xms也指定上與-Xmx相同就好)
適用場合:
- 很多的CPU計算任務而用戶交互任務較少的情況
- 不想自己去過多的關注GC參數(shù),想讓虛擬機自己進行調優(yōu)工作
- 對吞吐量要求較高,或需要達到一定的量。
(2.4)ParNew/CMS:
說明:
- 以上只是年老代CMS收集的過程,年輕代ParNew看"2.2、ParNew/Serial Old"就好
- CMS是多回收線程的,不要被上圖誤導,默認的線程數(shù):(CPU數(shù)量+3)/4
- CMS主要注重STW的縮短(該時間越短,用戶體驗越好,所以主要用于處理很多的交互任務的情況)
特點:
1.年輕代ParNew收集器采用多個GC線程實現(xiàn)"復制"算法(包括掃描、復制)
2.年老代CMS收集器采用多線程實現(xiàn)"標記-清除"算法
- 初始標記:標記與根集合節(jié)點直接關聯(lián)的節(jié)點。時間非常短,需要STW
- 并發(fā)標記:遍歷之前標記到的關聯(lián)節(jié)點,繼續(xù)向下標記所有存活節(jié)點。時間較長。
- 重新標記:重新遍歷trace并發(fā)期間修改過的引用關系對象。時間介于初始標記與并發(fā)標記之間,通常不會很長。需要STW
- 并發(fā)清理:直接清除非存活對象,清理之后,將該線程占用的CPU切換給用戶線程
3.初始標記與重新標記都會暫停所有用戶線程(即STW),但是時間較短;并發(fā)標記與并發(fā)清理時間較長,但是不需要STW
關于并發(fā)標記期間怎樣記錄發(fā)生變動的引用關系對象,在重新標記期間怎樣掃描這些對象
缺點:
- 并發(fā)標記與并發(fā)清理:按照說明的第二點來講,假設有2個CPU,那么其中有一個CPU會用于垃圾回收,而另一個用于用戶線程,這樣的話,之前是兩CPU運行用戶線程,現(xiàn)在是一個,那么效率就會急劇下降。也就是說,降低了吞吐量(即降低了CPU使用率)。
- 并發(fā)清理:在這一過程中,產(chǎn)生的垃圾無法被清理(因為發(fā)生在重新標記之后)
- 并發(fā)標記與并發(fā)清理:由于是與用戶線程并發(fā)的,所以用戶線程可能會分配對象,這樣既可能對象直接進入年老代(例如,大對象),也可能進入年輕代后,年輕代發(fā)生minor GC,這樣的話,實際上要求我們的年老代需要預留一定空間,也就是說要在年老代還有一定空間的情況下就要進行垃圾回收,留出一定內存空間來供其他線程使用,而不能等到年老代快爆滿了才進行垃圾回收,通過-XX:CMSInitiatingOccupancyFraction來指定當年老代空間滿了多少后進行垃圾回收
- 標記-清理算法:會產(chǎn)生內存碎片,由于是在老年代,可能會提前觸發(fā)Full GC(這正是我們要盡量減少的)
參數(shù)設置:
- -XX:+UseConcMarkSweepGC:使用該GC組合
- -XX:CMSInitiatingOccupancyFraction:指定當年老代空間滿了多少后進行垃圾回收
- -XX:+UseCMSCompactAtFullCollection:(默認是開啟的)在CMS收集器頂不住要進行FullGC時開啟內存碎片整理過程,該過程需要STW
- -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才進行整理
- -XX:ParallelCMSThreads:指定CMS回收線程的數(shù)量,默認為:(CPU數(shù)量+3)/4
適用場合:
用于處理很多的交互任務的情況
方法區(qū)的回收一般使用CMS,配置兩個參數(shù):-XX:+CMSPermGenSweepingEnabled與-XX:+CMSClassUnloadingEnabled
適用于一些需要長期運行且對相應時間有一定要求的后臺程序
(2.5)G1
說明:
- 從上圖來看,G1與CMS相比,僅在最后的"篩選回收"部分不同(CMS是并發(fā)清除),實際上G1回收器的整個堆內存的劃分都與其他收集器不同。
- CMS需要配合ParNew,G1可單獨回收整個空間
原理:
- G1收集器將整個堆劃分為多個大小相等的Region
- G1跟蹤各個region里面的垃圾堆積的價值(回收后所獲得的空間大小以及回收所需時間長短的經(jīng)驗值),在后臺維護一張優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的region,這種思路:在指定的時間內,掃描部分最有價值的region(而不是掃描整個堆內存),并回收,做到盡可能的在有限的時間內獲取盡可能高的收集效率。
運作流程:
- 初始標記:標記出所有與根節(jié)點直接關聯(lián)引用對象。需要STW
- 并發(fā)標記:遍歷之前標記到的關聯(lián)節(jié)點,繼續(xù)向下標記所有存活節(jié)點。在此期間所有變化引用關系的對象,都會被記錄在Remember Set Logs中
- 最終標記:標記在并發(fā)標記期間,新產(chǎn)生的垃圾。需要STW
- 篩選回收:根據(jù)用戶指定的期望回收時間回收價值較大的對象(看"原理"第二條)。需要STW
優(yōu)點:
- 停頓時間可以預測:我們指定時間,在指定時間內只回收部分價值最大的空間,而CMS需要掃描整個年老代,無法預測停頓時間
- 無內存碎片:垃圾回收后會整合空間,CMS采用"標記-清理"算法,存在內存碎片
- 篩選回收階段:
- 由于只回收部分region,所以STW時間我們可控,所以不需要與用戶線程并發(fā)爭搶CPU資源,而CMS并發(fā)清理需要占據(jù)一部分的CPU,會降低吞吐量。
- 由于STW,所以不會產(chǎn)生"浮動垃圾"(即CMS在并發(fā)清理階段產(chǎn)生的無法回收的垃圾)
適用范圍:
- 追求STW短:若ParNew/CMS用的挺好,就用這個;若不符合,用G1
- 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面沒有優(yōu)勢
以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持腳本之家!
相關文章
Java 數(shù)組獲取最大和最小值的實例實現(xiàn)
這篇文章主要介紹了Java 數(shù)組獲取最大和最小值的實例實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09解決myBatis中openSession()自動提交的問題
在學習MySQL過程中,發(fā)現(xiàn)插入操作自動提交,問題原因可能是myBatis中的openSession()方法設置了自動提交,或者是MySQL的默認引擎設置為不支持事務的MyISAM,解決辦法包括更改myBatis的提交設置或將MySQL表的引擎改為InnoDB2024-09-09MybatisPlus出現(xiàn)Error attempting to get col
本文重點分析使用@EnumValue注解轉換時遇到的一下錯誤原因,及解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-11-11SpringCloud之動態(tài)刷新、重試、服務化的實現(xiàn)
這篇文章主要介紹了SpringCloud 之動態(tài)刷新、重試、服務化的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10