JDK1.8之前的堆內存示意圖:
從上圖可以看出堆內存的分為新生代、老年代和永久代。新生代又被進一步分為:Eden 區+Survior1 區+Survior2 區。值得注意的是,在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。
JVM為什么要進行垃圾回收?
如果不進行垃圾回收,內存遲早都會被消耗空,因為我們在不斷的分配內存空間而不進行回收。除非內存無限大,我們可以任性的分配而不回收,但是事實並非如此。所以,垃圾回收是必須的。
垃圾收集的定義
- GC是垃圾收集的意思(Gabage Collection),Java提供的GC功能可以自動也只能自動地回收堆內存中不再使用的對象,釋放資源(目的),Java語言沒有提供釋放已分配內存的顯式操作方法(gc方法只是通知,不是立即執行)。
- 對於GC來說,當程序員創建對象時,GC就開始監控這個對象的地址、大小以及使用情況。
- 垃圾回收是一種動態存儲管理技術,它自動地釋放不再被程序引用的對象,當一個對象不再被引用的時候,按照特定的垃圾收集算法來實現資源自動回收的功能。
那什么時候進行垃圾回收呢?
1、第一種場景應該很明顯,當年輕代或者老年代滿了,Java虛擬機無法再為新的對象分配內存空間了,那么Java虛擬機就會觸發一次GC去回收掉那些已經不會再被使用到的對象
2、手動調用System.gc()方法,通常這樣會觸發一次的Full GC以及至少一次的Minor GC
3、程序運行的時候有一條低優先級的GC線程,它是一條守護線程,當這條線程處於運行狀態的時候,自然就觸發了一次GC了。這點也很好證明,不過要用到WeakReference的知識,后面寫WeakReference的時候會專門講到這個。
我們可以主動垃圾回收嗎?
- 每個 Java 應用程序都有一個 Runtime 類實例,使應用程序能夠與其運行的環境相連接。可以通過 getRuntime 方法獲取當前運行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的簡寫,兩者的行為沒有任何不同。
- 唯一的區別就是System.gc()寫起來比Runtime.getRuntime().gc()簡單點. 其實基本沒什么機會用得到這個命令, 因為這個命令只是建議JVM安排GC運行, 還有可能完全被拒絕。 GC本身是會周期性的自動運行的,由JVM決定運行的時機,而且現在的版本有多種更智能的模式可以選擇,還會根據運行的機器自動去做選擇,就算真的有性能上的需求,也應該去對GC的運行機制進行微調,而不是通過使用這個命令來實現性能的優化。
並行和並發的區別
並行(Parallel)
指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態;
如ParNew、Parallel Scavenge、Parallel Old;
Tips:通過名字就可以看出來,並行的都帶有Parallel關鍵字,ParNew的Par也是Parallel縮寫。
並發(Concurrent)
指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行);
用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上;
如CMS、G1(也有並行);
Minor GC和Full GC的區別
Minor GC
- 又稱新生代GC,指發生在新生代的垃圾收集動作;
- 因為Java對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快;
Full GC
- 又稱老年代GC,指發生在老年代的GC;
- Full GC速度一般比Minor GC慢10倍以上;
吞吐量(Throughput)
吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即
吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。
假設虛擬機總共運行了100分鍾,其中垃圾收集花掉1分鍾,那吞吐量就是99%。
四種引用
Java 中的垃圾回收一般是在 Java 堆中進行,因為堆中幾乎存放了 Java 中所有的對象實例。談到 Java 堆中的垃圾回收,自然要談到引用。在 JDK1.2 之后,Java 對引用的概念進行了擴充,將其分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。
有強到弱分為
- 強引用(Strong Reference)
- 軟引用(Soft Reference)
- 弱引用(Weak Reference)
- 虛引用(Phantom Reference)
強引用(Strong Reference)
如“Object obj = new Object()”,這類引用是 Java 程序中最普遍的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
強引用具備以下三個個特點:
- 強引用可以直接訪問目標對象;
- 強引用所指向的對象在任何時候都不會被系統回收。JVM寧願拋出OOM異常也不回收強引用所指向的對象;
- 強引用可能導致內存泄露;
軟引用(Soft Reference)
是用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在 JDK 1.2 之后,提供了 SoftReference 類來實現軟引用。
弱引用(Weak Reference)
用來描述非必須的對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發送之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。一旦一個弱引用對象被垃圾回收器回收,便會加入到一個注冊引用隊列中。在 JDK 1.2 之后,提供了 WeakReference類來實現弱引用。
Tips:軟引用、弱引用都非常適合來保存那些可有可無的緩存數據。如果這么做,當系統內存不足時,這些緩存數據會被回收,不會導致內存溢出。而當內存資源充足時,這些緩存數據又可以存在相當長的時間,從而起到加速系統的作用。
虛引用(Phantom Reference)
它是最弱的一種引用關系。一個持有虛引用的對象,和沒有引用幾乎是一樣的,隨時都有可能被垃圾回收器回收。當試圖通過虛引用的get()方法取得強引用時,總是會失敗。並且,虛引用必須和引用隊列一起使用,它的作用在於跟蹤垃圾回收過程。在 JDK 1.2 之后,提供了 PhantomReference類來實現虛引用。
Stop-The-World
GC在后台自動發起和自動完成的,在用戶不可見的情況下,把用戶正常的工作線程全部停掉,即GC停頓,會帶給用戶不良的體驗;
為什么要Stop-The-World
可達性分析的時候為了確保快照的一致性,需要對整個系統進行凍結,不可以出現分析過程中對象引用關系還在不斷變化的情況,也就是Stop-The-World。
Stop-The-World是導致GC卡頓的重要原因之一。
舉個例子:
你在做家務,正在計算家里有多少垃圾的時候,是不能允許別人在這時候清理或者增加垃圾的,否則你的計算將毫無意義。所以在這個時候,你需要把家里的人都關在門外,等你計算好垃圾的數量之后才能讓他們進來。
查看垃圾回收器
- 我們知道JVM分Client 和 Server模式。
- 如果啟動JVM不指定模式,JDK會根據當前的操作系統配置來啟動不同模式的JVM。
- 默認64bit操作系統下都會是Server模式的JVM。
# java -XX: +PrintCommandLineFlags –version
垃圾收集器
JVM是一個進程,垃圾收集器就是一個線程,垃圾收集線程是一個守護線程,優先級低,其在當前系統空閑或堆中老年代占用率較大時觸發。
JDK7/8后,HotSpot虛擬機所有收集器及組合(連線),如下圖:
圖中展示了7種不同分代的收集器:
- Serial
- ParNew (Serial的升級版,多線程)
- Parallel Scavenge
- Serial Old
- Parallel Old
- CMS
- G1
新生代收集器還是老年代收集器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆收集器:G1
吞吐量優先、停頓時間優先
- 吞吐量優先:Parallel Scavenge收集器、Parallel Old 收集器。
- 停頓時間優先:CMS(Concurrent Mark-Sweep)收集器。
吞吐量與停頓時間適用場景
- 停頓時間優先:交互多,對響應速度要求高
- 吞吐量優先:交互少,計算多,適合在后台運算的場景。
串行並行並發
- 串行:Serial、Serial Old
- 並行:ParNew、Parallel Scavenge、Parallel Old
- 並發:CMS、G1
算法
- 復制算法:Serial、ParNew、Parallel Scavenge、G1
- 標記-清除:CMS
- 標記-整理:Serial Old、Parallel Old、G1
1、Serial收集器
Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;
JDK1.3.1前是HotSpot新生代收集的唯一選擇;
特點
- 針對新生代
- 串行
- 復制算法
- 單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工作,
- 另一方面也意味着在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束為止,這個過程也稱為 Stop The world。
- 后者意味着,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這顯然對很多應用是難以接受的。
應用場景
對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。
Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器。 在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一百多MS),只要不頻繁發生,這是可以接受的
Tips:Stop the World是在用戶不可見的情況下執行的,會造成某些應用響應變慢;
Tips:因為新生代的特點是對象存活率低,所以收集算法用的是復制算法,把新生代存活對象復制到老年代,復制的內容不多,性能較好。
Tips:單線程地好處就是減少上下文切換,減少系統資源的開銷。但這種方式的缺點也很明顯,在GC的過程中,會暫停程序的執行。若GC不是頻繁發生,這或許是一個不錯的選擇,否則將會影響程序的執行性能。 對於新生代來說,區域比較小,停頓時間短,所以比較使用。
參數
- -XX:+UseSerialGC:串聯收集器
Tips:在JDK Client模式,不指定VM參數,默認是串行垃圾回收器
2、ParNew收集器
ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。除了使用多線程進行垃圾收集外,其余行為包括Serial收集器可用的所有控制參數、收集算法(復制算法)、Stop The World、對象分配規則、回收策略等與Serial收集器完全相同,兩者共用了相當多的代碼。
ParNew收集器的工作過程如下圖:
ParNew收集器除了使用多線程收集外,其他與Serial收集器相比並無太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一個具有划時代意義的收集器,具體內容將在稍后進行介紹。
ParNew 收集器在單CPU的環境中絕對不會有比Serial收集器有更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越。在多CPU環境下,隨着CPU的數量增加,它對於GC時系統資源的有效利用是很有好處的。
特點
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其余行為和Serial收集器完全一樣,包括Serial收集器可用的所有控制參數、收集算法、Stop The world、對象分配規則、回收策略等都一樣。在實現上也共用了相當多的代碼。
- 針對新生代
- 復制算法
- 串行
- 多線程
- GC時需要暫停所有用戶線程,直到GC結束
- Serial多線程版本,其他特點與Serial相同
應用場景
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。很重要的原因是:除了Serial收集器之外,目前只有它能與CMS收集器配合工作(看圖)。在JDK1.5時期,HotSpot推出了一款幾乎可以認為具有划時代意義的垃圾收集器-----CMS收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的並發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工作。
參數
- "-XX:+UseConcMarkSweepGC":指定使用CMS后,會默認使用ParNew作為新生代收集器;
- "-XX:+UseParNewGC":強制指定使用ParNew;
- "-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啟的收集線程與CPU的數量相同;
為什么只有ParNew能與CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意義上的並發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;
CMS作為老年代收集器,但卻無法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工作;
因為Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼框架,而另外獨立實現;而其余幾種收集器則共用了部分的框架代碼;
3、Parallel Scavenge收集器
Parallel Scavenge收集器和ParNew類似,新生代的收集器,同樣用的是復制算法,也是並行多線程收集。與ParNew最大的不同,它關注的是垃圾回收的吞吐量。
特點
- 針對新生代
- 復制算法
- 並行
- 多線程
- 高吞吐量為目標
應用場景
Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器。
高吞吐量為目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;適合那種交互少、運算多的場景
例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;
參數
- "-XX:+MaxGCPauseMillis":控制最大垃圾收集停頓時間,大於0的毫秒數;這個參數設置的越小,停頓時間可能會縮短,但也會導致吞吐量下降,導致垃圾收集發生得更頻繁。
- "-XX:GCTimeRatio":設置垃圾收集時間占總時間的比率,0<n<100的整數,就相當於設置吞吐量的大小。
先垃圾收集執行時間占應用程序執行時間的比例的計算方法是:
1 / (1 + n)
例如,選項-XX:GCTimeRatio=19,設置了垃圾收集時間占總時間的5%=1/(1+19);
默認值是1%--1/(1+99),即n=99;
垃圾收集所花費的時間是年輕一代和老年代收集的總時間;
此外,還有一個值得關注的參數:
"-XX:+UseAdptiveSizePolicy"
開啟這個參數后,就不用手工指定一些細節參數,如:
新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等;
JVM會根據當前系統運行情況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomiscs);
另外值得注意的一點是,Parallel Scavenge收集器無法與CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代選擇Parallel Scavenge收集器,老年代只有Serial Old收集器能與之配合使用。
Parallel Scavenge收集器 VS CMS等收集器:
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。
由於與吞吐量關系密切,Parallel Scavenge收集器也經常稱為“吞吐量優先”收集器。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器與ParNew收集器的一個重要區別是它具有自適應調節策略。
4、Serial Old收集器(標記-整理算法)
Serial收集器的工作流程如下圖:
如上圖所示,Serial 收集器在新生代和老年代都有對應的版本,除了收集算法不同,兩個版本並沒有其他差異。
- Serial 新生代收集器采用的是復制算法。
- Serial Old 老年代采用的是標記 - 整理算法。
特性
- Serial Old是Serial的老年代版本,
- 除了采用標記-整理算法,其他與Serial相同
應用場景
-
- Client模式
Serial Old收集器的主要意義也是在於給Client模式下的虛擬機使用。 - Server模式
如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;另一種用途就是作為CMS收集器的后備預案,在並發收集發生"Concurrent Mode Failure"時使用。
- Client模式
5、Parallel Old收集器
如上圖所示,Parallel 收集器在新生代和老年代也都有對應的版本,除了收集算法不同,兩個版本並沒有其他差異。
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6開始提供。
Mark-Compact
特點
- Parallel Old是Parallel Scavenge的老年代版本
- Parallel Old 老年代采用的是標記 - 整理算法,其他特點與Parallel Scavenge相同
使用場景
- 在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器組合。
- JDK1.6及之后用來代替老年代的Serial Old收集器;
- 特別是在Server模式,多CPU的情況下;
參數
- -XX:+UseParallelOldGC:指定使用Parallel Old收集器;
6、CMS(Concurrent Mark Sweep)收集器
概述
- CMS是HotSpot在JDK5推出的第一款真正意義上的並發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;
- 命名中用的是concurrent,而不是parallel,說明這個收集器是有與工作執行並發的能力的。MS則說明算法用的是Mark Sweep算法。
- 它關注的是垃圾回收最短的停頓時間(低停頓),在老年代並不頻繁GC的場景下,是比較適用的。
特點
- 針對老年代
- 標記-清除算法 (不進行壓縮操作,產生內存碎片);
- 並發
- 多線程
- 收集過程中不需要暫停用戶線程
- 以獲取最短回收停頓時間為目標
應用場景
與用戶交互較多的場景。CMS 收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網或者B/S系統的服務端上,這類應用尤其注重服務的響應速度,希望系統停頓時間最短,以給用戶帶來極好的體驗。CMS收集器就非常符合這類應用的需求。
CMS是一種以獲取最短回收停頓時間為目標的收集器。在重視響應速度和用戶體驗的應用中,CMS應用很多。
CMS GC過程分四步完成:
比前面幾種收集器更復雜,可以分為4個步驟:
1、初始標記(initial mark)
- 單線程執行
- 需要“Stop The World”
- 但僅僅把GC Roots的直接關聯可達的對象給標記一下,由於直接關聯對象比較小,所以這里的速度非常快
2、並發標記(concurrent mark)
- 對於初始標記過程所標記的初始標記對象,進行並發追蹤標記,
- 此時其他線程仍可以繼續工作。
- 此處時間較長,但不停頓。
- 並不能保證可以標記出所有的存活對象;
3、重新標記(remark)
- 在並發標記的過程中,由於可能還會產生新的垃圾,所以此時需要重新標記新產生的垃圾。
- 此處執行並行標記,與用戶線程不並發,所以依然是“Stop The World”,
- 且停頓時間比初始標記稍長,但遠比並發標記短。
4、並發清除(concurrent sweep)
- 並發清除之前所標記的垃圾。
- 其他用戶線程仍可以工作,不需要停頓。
Tips:其中,初始標記和並發標記仍然需要Stop the World、初始標記僅僅標記一下GC Roots能直接關聯到的對象,速度很快,並發標記就是進行GC RootsTracing的過程,而重新標記階段則是為了修正並發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段長,但遠比並發標記的時間短。
由於整個過程中耗時最長的並發標記和並發清除過程收集器線程都可以與用戶線程一起工作,所以整體上說,CMS收集器的內存回收過程是與用戶線程一共並發執行的。
參數
- -XX:+UseConcMarkSweepGC:使用CMS收集器
- -XX:+ UseCMSCompactAtFullCollection:Full GC后,進行一次碎片整理;整理過程是獨占的,會引起停頓時間變長
- -XX:+CMSFullGCsBeforeCompaction:設置進行幾次Full GC后,進行一次碎片整理
- -XX:ParallelCMSThreads:設定CMS的線程數量(一般情況約等於可用CPU數量)
缺點
總體來看,與Parallel Old垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間;
但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要占用更大的堆空間;
由於最耗費時間的並發標記與並發清除階段都不需要暫停工作,所以整體的回收是低停頓的。
由於CMS以上特性,缺點也是比較明顯的,
1、對CPU資源非常敏感
對CPU資源非常敏感 其實,面向並發設計的程序都對CPU資源比較敏感。
在並發階段,它雖然不會導致用戶線程停頓,但會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。
CMS默認啟動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,並發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。
但是當CPU不足4個時(比如2個),CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%,其實也讓人無法接受。
並發收集雖然不會暫停用戶線程,但因為占用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低。
CMS的默認收集線程數量是=(CPU數量+3)/4;
當CPU數量多於4個,收集線程占用的CPU資源多於25%,對用戶程序影響可能較大;不足4個時,影響更大,可能無法接受。
2、浮動垃圾(Floating Garbage)
由於CMS並發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
由於在垃圾收集階段用戶線程還需要運行,那就還需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,也可以熱為CMS所需要的空間比其他垃圾收集器大;
"-XX:CMSInitiatingOccupancyFraction":設置CMS預留內存空間;
JDK1.5默認值為68%;
JDK1.6變為大約92%;
3、"Concurrent Mode Failure"失敗
如果CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣會導致另一次Full GC的產生。這樣停頓時間就更長了,代價會更大,所以 "-XX:CMSInitiatingOccupancyFraction"不能設置得太大。
4、產生大量內存碎片
這個問題並不是CMS的問題,而是算法的問題。由於CMS基於"標記-清除"算法,清除后不進行壓縮操作,所以會產生碎片
"標記-清除"算法介紹時曾說過:
產生大量不連續的內存碎片會導致分配大內存對象時,無法找到足夠的連續內存,從而需要提前觸發另一次Full GC動作。
4.1碎片解決方法:
(1)、"-XX:+UseCMSCompactAtFullCollection"
使得CMS出現上面這種情況時不進行Full GC,而開啟內存碎片的合並整理過程;
但合並整理過程無法並發,停頓時間會變長;
默認開啟(但不會進行,結合下面的CMSFullGCsBeforeCompaction);
(2)、"-XX:+CMSFullGCsBeforeCompaction"
設置執行多少次不壓縮的Full GC后,來一次壓縮整理;
為減少合並整理過程的停頓時間;
默認為0,也就是說每次都執行Full GC,不會進行壓縮整理;
由於空間不再連續,CMS需要使用可用"空閑列表"內存分配方式,這比簡單實用"碰撞指針"分配內存消耗大;
7、G1收集器
概述
- G1(Garbage - First)名稱的由來是G1跟蹤各個Region里面的垃圾堆的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。
- G1(Garbage-First)是JDK7-u4才推出商用的收集器;
注意:G1與前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了!
這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。
取而代之的是,G1算法將堆划分為若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味着,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1划分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
PS:在java 8中,持久代也移動到了普通的堆內存空間中,改為元空間。
特點
G1除了降低停頓外,還能建立可預測的停頓時間模型;
1、Region概念
- 橫跨整個堆內存
- 在G1之前的其他收集器進行收集的范圍都是整個新生代或者老生代,而G1不再是這樣。
- G1在使用時,Java堆的內存布局與其他收集器有很大區別,
- 它將整個Java堆划分為多個大小相等的獨立區域(Region),
- 雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,而都是一部分Region(可以不連續)的集合。
2、可並行,可並發
能充分利用多CPU、多核環境下的硬件優勢;
G1 能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短“Stop The World”停頓時間
並行:使用多個CPU來縮短Stop-The-World停頓的時間,
並發:也可以並發讓垃圾收集與用戶程序同時進行
3、分代收集,收集范圍包括新生代和老年代
- 能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;
- 能夠采用不同方式處理不同時期的對象;
4、空間整合,不產生碎片
- 從整體看,是基於標記-整理算法;
- 從局部(兩個Region間)看,是基於復制算法;
- 都不會產生內存碎片,有利於長時間運行;
- 這種特性有利於程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。
5、可預測的停頓:低停頓的同時實現高吞吐量
- G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計划地避免在整個Java堆中進行全區域的垃圾收集。
- G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后台維護一個優先列表,
- 每次根據允許的收集時間,優先回收價值最大的Region,這樣就保證了在有限的時間內盡可能提高效率。(這也就是Garbage-First名稱的來由)。
- 這種使用Region划分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
應用場景
如果你的應用追求低停頓,那G1現在已經可以作為一個可嘗試選擇,如果你的應用追求吞吐量,那G1並不會為你帶來什么特別的好處。
1.面向服務端應用,針對具有大內存、多處理器的機器;最主要的應用是為需要低GC延遲,並具有大堆的應用程序提供解決方案;
如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;
2.用來替換掉JDK1.5的CMS收集器;
(1)、超過50%的Java堆被活動數據占用;
(2)、對象分配頻率或年代提升頻率變化很大;
(3)、GC停頓時間過長(長與0.5至1秒)。
參數
- "-XX:+UseG1GC":指定使用G1收集器;
- "-XX:InitiatingHeapOccupancyPercent":當整個Java堆的占用率達到參數值時,開始並發標記階段;默認為45;
- "-XX:MaxGCPauseMillis":為G1設置暫停時間目標,默認值為200毫秒;
- "-XX:G1HeapRegionSize":設置每個Region大小,范圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region;
為什么G1收集器可以實現可預測的停頓?
G1可以建立可預測的停頓時間模型,是因為:
可以有計划地避免在Java堆的進行全區域的垃圾收集;
G1跟蹤各個Region獲得其收集價值大小,在后台維護一個優先列表;
每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);
這就保證了在有限的時間內可以獲取盡可能高的收集效率;
G1收集器運作過程
不計算維護Remembered Set的操作,可以分為4個步驟(與CMS較為相似)。
1、初始標記(Initial Marking)
- 初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,
- 速度很快,
- 需要“Stop The World”。(OopMap)
2、並發標記(Concurrent Marking)
- 進行GC Roots Tracing的過程,從剛才產生的集合中標記出存活對象;(也就是從GC Roots 開始對堆進行可達性分析,找出存活對象。)
- 耗時較長,但應用程序也在運行;
- 並不能保證可以標記出所有的存活對象;
3、最終標記(Final Marking)
- 最終標記和CMS的重新標記階段一樣,也是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,
- 這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,
- 也需要“Stop The World”。(修正Remebered Set)
4、篩選回收(Live Data Counting and Evacuation)
- 首先排序各個Region的回收價值和成本;
- 然后根據用戶期望的GC停頓時間來制定回收計划;
- 最后按計划回收一些價值高的Region中垃圾對象;
- 回收時采用"復制"算法,從一個或多個Region復制存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;
- 可以並發進行,降低停頓時間,並增加吞吐量;
參數
- "-XX:+UseG1GC":指定使用G1收集器;
- "-XX:InitiatingHeapOccupancyPercent":當整個Java堆的占用率達到參數值時,開始並發標記階段;默認為45;
- "-XX:MaxGCPauseMillis":為G1設置暫停時間目標,默認值為200毫秒;
- "-XX:G1HeapRegionSize":設置每個Region大小,范圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個
總結
圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。
虛擬機所處的區域,則表示它是屬於新生代收集器還是老年代收集器。
收集器總結
收集器 |
串行、並行or並發 |
新生代/老年代 |
算法 |
目標 |
適用場景 |
Serial |
串行 |
新生代 |
復制算法 |
響應速度優先 |
單CPU環境下的Client模式 |
Serial Old |
串行 |
老年代 |
標記-整理 |
響應速度優先 |
單CPU環境下的Client模式、CMS的后備預案 |
ParNew |
並行 |
新生代 |
復制算法 |
響應速度優先 |
多CPU環境時在Server模式下與CMS配合 |
Parallel Scavenge |
並行 |
新生代 |
復制算法 |
吞吐量優先 |
在后台運算而不需要太多交互的任務 |
Parallel Old |
並行 |
老年代 |
標記-整理 |
吞吐量優先 |
在后台運算而不需要太多交互的任務 |
CMS |
並發 |
老年代 |
標記-清除 |
響應速度優先 |
集中在互聯網站或B/S系統服務端上的Java應用 |
G1 |
並發 |
both |
標記-整理+復制算法 |
響應速度優先 |
面向服務端應用,將來替換CMS |
參數總結
參數 |
MinorGC |
Full GC |
描述 |
-XX:+UseSerialGC |
Serial收集器串行回收 |
Serial Old收集器串行回收 |
該選項可以手動指定Serial收集器+Serial Old收集器組合執行內存回收 |
-XX:+UseParNewGC |
ParNew收集器並行回收 |
Serial Old收集器串行回收 |
該選項可以手動指定ParNew收集器+Serilal Old組合執行內存回收 |
-XX:+UseParallelGC |
Parallel收集器並行回收 |
Serial Old收集器串行回收 |
該選項可以手動指定Parallel收集器+Serial Old收集器組合執行內存回收 |
-XX:+UseParallelOldGC |
Parallel收集器並行回收 |
Parallel Old收集器並行回收 |
該選項可以手動指定Parallel收集器+Parallel Old收集器組合執行內存回收 |
-XX:+UseConcMarkSweepGC |
ParNew收集器並行回收 |
缺省使用CMS收集器並發回收,備用采用Serial Old收集器串行回收 |
該選項可以手動指定ParNew收集器+CMS收集器+Serial Old收集器組合執行內存回收。優先使用ParNew收集器+CMS收集器的組合,當出現ConcurrentMode Fail或者Promotion Failed時,則采用ParNew收集器+Serial Old收集器的組合 |
-XX:+UseConcMarkSweepGC -XX:-UseParNewGC |
Serial收集器串行回收 |
||
-XX:+UseG1GC |
G1收集器並發、並行執行內存回收 |
暫無 |
ZGC
概述
在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高並發的收集器。
ZGC幾乎在所有地方並發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際是非常少的。那么其他階段是怎么做到可以並發執行的呢?
ZGC主要新增了兩項技術,一個是着色指針Colored Pointer,另一個是讀屏障Load Barrier。
ZGC 是一個並發、基於區域(region)、增量式壓縮的收集器。Stop-The-World 階段只會在根對象掃描(root scanning)階段發生,這樣的話 GC 暫停時間並不會隨着堆和存活對象的數量而增加。
ZGC 的設計目標
- TB 級別的堆內存管理;
- 最大 GC Pause 不高於 10ms;
- 最大的吞吐率(Throughput)損耗不高於 15%;
關鍵點:GC Pause 不會隨着 堆大小的增加 而增大。
ZGC 中關鍵技術
- 加載屏障(Load barriers)技術;
- 有色對象指針(Colored pointers);
- 單一分代內存管理(這一點很有意思);
- 基於區域的內存管理;
- 部分內存壓縮;
- 即時內存復用。
並行化處理階段
- 標記(Marking);
- 重定位(Relocation)/壓縮(Compaction);
- 重新分配集的選擇(Relocation set selection);
- 引用處理(Reference processing);
- 弱引用的清理(WeakRefs Cleaning);
- 字符串常量池(String Table)和符號表(Symbol Table)的清理;
- 類卸載(Class unloading)。
着色指針Colored Pointer
ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked1、Marked0(ZGC僅支持64位平台),以標記該指向內存的存儲狀態。相當於在對象的指針上標注了對象的信息。注意,這里的指針相當於Java術語當中的引用。
在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
在G1的時候就說到過,Compact階段是需要STW,否則會影響用戶線程執行。那么怎么解決這個問題呢?
讀屏障Load Barrier
由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),若請求讀的內存在被着色了,那么則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有一定的耗費,從而達到與用戶線程並發的效果。
把這兩項技術聯合下理解,引用R大(RednaxelaFX)的話
與標記對象的傳統算法相比,ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新為有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在為了保持應用與GC一致而粗暴整體的Stop The World。
ZGC雖然目前還在JDK 11還在實驗階段,但由於算法與思想是一個非常大的提升,相信在未來不久會成為主流的GC收集器使用。
參數
ZGC回收機預計在jdk11支持,ZGC目前僅適用於Linux / x64 。和G1開啟很像,用下面參數即可開啟:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
參考
https://www.cnblogs.com/haitaofeiyang/p/7811311.html
https://crowhawk.github.io/2017/08/15/JVM_3/
https://blog.csdn.net/qian520ao/article/details/79050982
https://zackku.com/JVM-gc-collector/
http://tang.love/2018/03/06/z-garbage-collector/
https://blog.csdn.net/high2011/article/details/80177473
https://blog.csdn.net/sunjin9418/article/details/79603651
https://blog.csdn.net/tjiyu/article/details/53983650