參考博客:https://www.cnblogs.com/parryyang/p/5748711.html
參考博客: https://blog.csdn.net/dongyuxu342719/article/details/78835431
概述
1、內存回收的區域主要在堆和方法區,虛擬機棧和本地方法棧的內存分配與回收具有確定性。
在Java內存運行時區域的各個部分中,程序計數器、虛擬機棧、本地方法棧3個部分是線程私有的,隨線程而生,隨線程而滅;棧中的棧幀隨著方法進入和退出而執行著入棧和出棧的操作。每一個棧幀中分配多少內存基本上在類結構確定下來時就已知,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多的考慮回收的問題,因為方法結束或線程結束時,內存自然就隨著回收了。
而在Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存可能不一樣,我們只有在程序運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。
2、如何判斷哪些對象需要回收?
接下來介紹的引用計數法和可達性分析法,引用計數法具有局限性,不能回收循環引用的對象。
3、如何回收對象?
本章介紹的垃圾收集算法回答了這個問題。現在商用的垃圾回收機制一般使用分代收集算法,年輕代使用復制算法,老年代使用標記整理法。
一、對象已死嗎
1、引用計數法
引用計數法是指給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時候計數器值為0就表示不會再被任何對象使用。
客觀的說,引用計數法(Reference Counting)的實現簡單,判斷效率也很高,在大部分情況下都是一個不錯的算法。但在主流的Java虛擬機里面沒有使用引用計數法來管理內存,主要原因是它很難解決對象之間相互循環引用的問題。
引用計數無法解決下面兩個對象相互引用但不可達的問題,但運行代碼后發現對象實際上能夠被GC。
public class ReferenceCountingGC {
2. public Object instance=null;
3. private static final int _1MB=1024*1024;
4. private byte [] bigSize=new byte[2*_1MB];
5.
6. public static void testGC(){
7. ReferenceCountingGC objA=new ReferenceCountingGC();
8. ReferenceCountingGC objB=new ReferenceCountingGC();
9. objA.instance=objB;
10. objB.instance=objA;
11. objA=null;
12. objB=null;
13. System.gc();
14. }
15. public static void main(String []args){
16. testGC();
17. }
18.}
2、可達性分析算法
在主流的商用程序語言(Java、C#)的主流實現中,都是通過可達性分析(ReachabilityAnalysis)來判斷對象是否存活的。這個算法的基本思路是通過一系列稱為GC Roots的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈(就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即Native方法)引用的對象。
二、垃圾收集算法
1、標記-清除法
分為“標記”和“清除”兩個階段:首先標記出需要回收的所有對象,在標記完成后統一回收標記的對象,標記過程就是之前講的通過引用計數法和可達性分析法進行判定。
它的主要不足有兩個:
效率問題:標記和清除的效率都不高,
空間問題:標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致在需要分配較大對象時,無法找到連續的內存空間而不得不提前觸發另一次垃圾收集動作。
2、復制算法
將內存劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊內存用完了,就將還存活的對象復制到另一塊內存上,然后把已使用過的內存空間一次清理掉。
優點:每次只對其中一塊進行GC,不用考慮內存碎片的問題,并且實現簡單,運行高效
缺點:內存縮小了一半
注:現在商用虛擬機都采用這種算法來回收新生代,因為新生代中98%的對象都是朝生夕死的,所以并不需要1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一塊Survivor空間。當回收時,將Eden和Survivor中開存活的對象一次性復制到另一塊Survivor空間上,最后清理掉Eden和剛才使用過的Survivor空間。
HotSpot虛擬機默認Eden和Survivor的大小是8:1,也就是每次新生代可用空間是整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%可回收只是一般的情況的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
內存的分配擔保類似于銀行貸款,如果我們的信譽好。在98%的情況下都能按時償還,于是銀行可能默認我們下次也能按時償還貸款,只需要有一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了。內存的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
3、標記-整理法
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費另外50%的空間,就需要額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程任然與“標記-清除”算法的標記過程相同,但后續步驟不是對可回收對象進行直接清理,而是讓所有活的對象都移動到另一端,然后直接清理掉端邊界以外的內存,
4、分代收集算法
當前商業虛擬機的垃圾收集都采用“分代收集”(GenerationalCollection)算法,這種算法只是根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆劃分為新生代和老年代,這樣就可以根據各個年代的特點采用最適合的收集算法。在新生代中每次都有大量的對象死去,只有少量存活,那就采用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它們進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
5、HotPot 分代收集算法
對象將根據存活的時間被分為:年輕代、年老代、永久代。
年輕代:
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被復制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被復制到另外一個Survivor區。同時,根據程序需要,Survivor區是可以配置為多個的(多于兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。
年老代:
在年輕代中經歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。
持久代:
用于存放靜態文件,如Java類、方法等。
Scavenge GC
一般情況下,Eden空間滿時,就會觸發Scavenge GC,清除Eden區非存活對象,并且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。Eden區的GC會頻繁進行,速度也很快。
Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個對進行回收,所以比Scavenge GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對于FullGC的調節。
三、安全點:Stop the world
1、Stop the world
GC 操作的某些階段,如可達性分析遍歷 GC ROOT 節點找引用鏈,需要確保在一致性的快照中進行(一致性的意思指分析期間整個執行系統好像凍結在某個時間節點上,不可以出現分析過程中對象引用關系還在不斷變化的情況,該點不滿足分析的準確性就無法保證)。這是導致GC進行時必須停頓所有的執行線程的其中一個重要原因。(Sun 將這件事情稱之為 “Stop the world”。)即使是在號稱幾乎不會發生停頓的CMS收集器中,枚舉根節點時也是必須停頓的。
2、OopMap
OopMap:在所有執行線程停頓下來后,虛擬機并不需要逐個檢查每個棧的局部變量表來查找引用的位置,而應當有辦法直接得知哪些地方存著對象的應用。在執行到某條指令時,棧中什么位置存放了什么變量是確定的,在編譯期間虛擬機就可以計算出這些信息。在 HotSpot 的實現中,用一組稱為 OopMap 的數據結構記錄了某一些執行節點哪些位置是引用,這樣,在GC掃描時就可以直接獲取這些信息了。
3、SafePoint
在 OopMap 的協助下,HotSpot 可以快速且準確地完成 GC Roots 枚舉。但是隨著程序的運行,引用關系會隨之變化,虛擬機不可能為每一個執行節點都生成 OopMap 來記錄引用關系,那將需要大量額外的空間,GC的成本將會變得很高。實際上,HotSpot虛擬機也的確沒有為每一條指令都生成OopMap,只是在特定位置記錄這些信息,這些位置稱為“安全點”(Safepoint),即程序執行時并非在所有地方都停頓下來開始GC,只有在到達安全點時才能暫停。
Safepoint既不能選的太少導致GC等待太長時間,也不能過于頻繁以至于過分增大運行時的負荷。安全點一般選取在方法調用、循環跳轉、異常跳轉等讓程序長時間執行的指令。
對于Safepoint,另一個需要考慮的問題是如何在GC發生時讓所有線程(這里不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設置一個標志,各線程執行時主動去輪詢這個標志,當發現中斷標志為真時就自己中斷掛起。輪詢標志的位置和安全點是重合的,另外再加上創建對象需要分配內存的地方。
4、SafeRegion
Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是程序不執行的時候呢?不執行就是程序沒有分配到CPU時間,典型的例子就是線程處于Sleep狀態或Blocked狀態,這時候線程無法響應JVM的中斷請求,“走”到安全的地方再掛起。JVM顯然也不會等待線程重新分配CPU時間。對于這種情況就需要安全區域(Safe Region)來解決。
安全區域是指在一段代碼片段中引用關系不會發生變化,在這個區域的任意地方開始GC都是安全的。我們也把Safe Region稱為擴展的Safepoint。
在線程執行到Safe Region中的代碼的時候,首先標識自己進入了Safe Region,這樣,當在這段時間里JVM要發起GC時,就不用考慮標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉()或者整個GC過程如果完成了,那線程就繼續執行,否則它就必須等待收到可以安全離開Safe Region的信號為止。
四、垃圾收集器
參考博客:https://blog.csdn.net/tjiyu/article/details/53983650
垃圾收集器是垃圾回收算法(標記-清除算法、復制算法、標記-整理算法)的具體實現,不同商家、不同版本的JVM所提供的垃圾收集器可能會有很在差別,下面主要介紹HotSpot虛擬機中的垃圾收集器。
本節介紹這些收集器的特性、基本原理和使用場景。沒有最好的收集器,更沒有萬能的收集,選擇的只能是適合具體應用場景的收集器。
1、垃圾收集器組合
JDK7/8后,HotSpot虛擬機所有收集器及組合(連線),如下圖:
- 圖中展示了7種不同分代的收集器:
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
-
它們所處區域,表明其是屬于新生代收集器還是老年代收集器:
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
兩個收集器間有連線,表明它們可以搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
- 其中Serial Old作為CMS出現"Concurrent Mode Failure"失敗的后備預案。
2、 Minor GC 和 Full GC
- Minor GC
又稱新生代GC,指發生在新生代的垃圾收集動作;
因為Java對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快;
- Full GC
又稱Major GC或老年代GC,指發生在老年代的GC;
出現Full GC經常會伴隨至少一次的Minor GC(不是絕對,Parallel Sacvenge收集器就可以選擇設置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;
3、Serial 收集器(*選學)
Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一選擇。
特點: 針對新生代;采用復制算法; 單線程收集;進行垃圾收集時,必須暫停所有工作線程,直到完成。
應用場景:依然是HotSpot在Client模式下默認的新生代收集器;也有優于其他收集器的地方:簡單高效(與其他收集器的單線程相比);對于限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一百多MS),只要不頻繁發生,這是可以接受的。
Serial/Serial Old組合收集器運行示意圖如下:
4、ParNew收集器
ParNew垃圾收集器是Serial收集器的多線程版本。
除了多線程外,其余的行為、特點和Serial收集器一樣;如Serial收集器可用控制參數、收集算法、Stop The World、內存分配規則、回收策略等;兩個收集器共用了不少代碼;
應用場景:在Server模式下,ParNew收集器是一個非常重要的收集器,因為除Serial外,目前只有它能與CMS收集器配合工作;但在單個CPU環境中,不會比Serail收集器有更好的效果,因為存在線程交互開銷。
-
ParNew/Serial Old組合收集器運行示意圖如下:image
5、Parallel Scavenge收集器
Parallel Scavenge垃圾收集器因為與吞吐量關系密切,也稱為吞吐量收集器(Throughput Collector)。
特點:有一些特點與 ParNew 收集器相似,新生代收集器;采用復制算法;多線程收集。主要特點是:Parallel Scavenge 收集器的目標則是達一個可控制的吞吐量(Throughput),系統通過調節新生老生代空間比例等,來調節吞吐量。
Parallel Scavenge 收集器提供了兩個參數用來精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis,以及直接設置吞吐量大小 -XXGCTimeRatio。停頓時間和吞吐量存在相互制約的關系:GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一點,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾收集發生的更頻繁一些,原來10收集一次,現在變成5秒收集一次,每次停頓70毫秒,停頓時間的確是下降了,但吞吐量也降下來了。
應用場景: 高吞吐量為目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;當應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求時,即程序主要在后臺進行計算,而不需要與用戶進行太多交互;例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;
-
吞吐量與收集器關注點說明
(A)、吞吐量(Throughput)
CPU用于運行用戶代碼的時間與CPU總消耗時間的比值;
即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);
高吞吐量即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;
(B)、垃圾收集器期望的目標(關注點)
(1)、停頓時間
停頓時間越短就適合需要與用戶交互的程序;
良好的響應速度能提升用戶體驗;
(2)、吞吐量
高吞吐量則可以高效率地利用CPU時間,盡快完成運算的任務;
主要適合在后臺計算而不需要太多交互的任務;
(3)、覆蓋區(Footprint)
在達到前面兩個目標的情況下,盡量減少堆的內存空間;
可以獲得更好的空間局部性;
6、Serial Old收集器(*選學)
Serial Old是 Serial收集器的老年代版本;
針對老年代;采用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);單線程收集;
應用場景:主要用于Client模式;而在Server模式有兩大用途:(A)、在JDK1.5及之前,與Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);(B)、作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用(后面詳解);
7、Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本; JDK1.6中才開始提供;
針對老年代;采用"標記-整理"算法;多線程收集;
JDK1.6及之后用來代替老年代的Serial Old收集器;特別是在Server模式,多CPU的情況下;這樣在注重吞吐量以及CPU資源敏感的場景,就有了Parallel Scavenge加Parallel Old收集器的"給力"應用組合;
8、CMS收集器
并發標記清理(Concurrent Mark Sweep,CMS)收集器也稱為并發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器;CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間;
特點:針對老年代;基于"標記-清除"算法(不進行壓縮操作,產生內存碎片);以獲取最短回收停頓時間為目標;并發收集、低停頓;需要更多的內存;是HotSpot在JDK1.5推出的第一款真正意義上的并發(Concurrent)收集器;第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;
-
缺點:
(A)對CPU資源非常敏感:并發收集雖然不會暫停用戶線程,但因為占用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低。
(B)無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗:在并發清除時,用戶線程新產生的垃圾,稱為浮動垃圾;這使得并發清除時需要預留一定的內存空間,不能像其他收集器在老年代幾乎填滿再進行收集;如果CMS預留內存空間無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗;這時JVM啟用后備預案:臨時啟用Serail Old收集器,而導致另一次Full GC的產生;
(C)產生大量內存碎片:由于CMS基于"標記-清除"算法,清除后不進行壓縮操作;
適用場景:與用戶交互較多的場景,希望系統停頓時間最短,注重服務的響應速度;以給用戶帶來較好的體驗;如常見WEB、B/S系統的服務器上的應用;
-
CMS收集器運行示意圖如下:image
9、G1收集器
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
-
特點:
(A)并行與并發:能充分利用多CPU、多核環境下的硬件優勢;可以并行來縮短"Stop The World"停頓時間;也可以并發讓垃圾收集與用戶程序同時進行;
(B)分代收集,收集范圍包括新生代和老年代 :能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;能夠采用不同方式處理不同時期的對象;雖然保留分代概念,但Java堆的內存布局有很大差別;將整個堆劃分為多個大小相等的獨立區域(Region);新生代和老年代不再是物理隔離,它們都是一部分Region(不需要連續)的集合;
(C)結合多種垃圾收集算法,空間整合,不產生碎片。從整體看,是基于標記-整理算法;從局部(兩個Region間)看,是基于復制算法;這是一種類似火車算法的實現;
(D)可預測的停頓:低停頓的同時實現高吞吐量G1除了追求低停頓處,還能建立可預測的停頓時間模型;可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒;
-
應用場景:面向服務端應用,針對具有大內存、多處理器的機器;最主要的應用是為需要低GC延遲,并具有大堆的應用程序提供解決方案;如:在堆大小約6GB或更大時,可預測的暫停時間可以低于0.5秒; 用來替換掉JDK1.5中的CMS收集器;在下面的情況時,使用G1可能比CMS好:
(1)超過50%的Java堆被活動數據占用;
(2)對象分配頻率或年代提升頻率變化很大;
(3)GC停頓時間過長(長于0.5至1秒)。 是否一定采用G1呢?也未必:如果現在采用的收集器沒有出現問題,不用急著去選擇G1;如果應用程序追求低停頓,可以嘗試選擇G1;是否代替CMS需要實際場景測試才知道。
-
G1 收集器運行示意圖如下:image
五、內存分配與回收策略
對象優先在 Eden 分配
大對象直接進入老年代:所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續內存空間來存放他們。虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區以及兩個Survivor區之間發生大量的內存復制(新生代使用復制算法收集內存)。
長期存活的對象將進入老年代:虛擬機給每個對象定義了一個對象年齡計數器。如果對象在Eden出生,并經過第一次Minor GC后仍然存活,并且能被Survivor所容納的話,將被移動到Survivor區,并且對象年齡設置為1。對象在Survivor區中沒“熬過”一次Minor GC,年齡就增加1,當它的年齡增加到一定程度(默認15歲),就將會晉升到老年代中。對象晉升到老年代的年齡閥值,可以通過參數-XX:MaxTenuringThreshold設置。
動態對象年齡判定:為了能更好的適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
空間分配擔保:在發生Minor GC之前,虛擬機會首先檢查老年代最大可用連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可用確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試第一次Minor GC,即使這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置為不允許冒險,那這時也要改為進行一次Full GC。如果出現HandlePromotionFailure失敗,那就只好在失敗之后重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁。