JVM GC回收哪個區域內的垃圾?
需要注意的是,JVM GC只回收堆區和方法區內的對象。而棧區的數據,在超出作用域后會被JVM自動釋放掉,所以其不在JVM GC的管理范圍內。
Java方法區在Sun HotSpot虛擬機中被稱為永久代,很多人認為該部分的內存是不用回收的,java虛擬機規范也沒有對該部分內存的垃圾收集做規定,但是方法區中的廢棄常量和無用的類還是需要回收以保證永久代不會發生內存溢出。
判斷廢棄常量的方法:如果常量池中的某個常量沒有被任何引用所引用,則該常量是廢棄常量。
判斷無用的類:
(1).該類的所有實例都已經被回收,即java堆中不存在該類的實例對象。
(2).加載該類的類加載器已經被回收。
(3).該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機制訪問該類的方法。
JVM GC怎么判斷對象可以被回收了?
1)對象沒有引用
2)作用域發生未捕獲異常
3)程序在作用域正常執行完畢
4)程序執行了System.exit()
5) 程序發生意外終止(被殺線程等)
JVM GC什么時候執行?
eden區空間不夠存放新對象的時候,執行Minro GC。升到老年代的對象大於老年代剩余空間的時候執行Full GC,或者小於的時候被HandlePromotionFailure 參數強制Full GC 。
調優主要是減少 Full GC 的觸發次數,可以通過 NewRatio 控制新生代轉老年代的比例,通過MaxTenuringThreshold 設置對象進入老年代的年齡閥值(后面會介紹到)。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:設置JVM最大可用內存為3550m。
-Xms3550m:設置JVM初始內存為3550m。
此值可以設置與 -Xmx 相同,以避免每次垃圾回收完成后JVM重新分配內存。
-Xmn2g:設置年輕代大小為2G。整個堆大小=年輕代大小+老年代大小+永久代大小。永久代一般固定大小為64m,所以增大年輕代后,將會減小老年代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
-Xss128k:設置每個線程的堆棧大小。JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256k。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與老年代的比值(除去永久代)。設置為4,則年輕代與老年代所占比值為1:4,年輕代占整個堆棧的1/5。
-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值。設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6。
-XX:MaxPermSize=16m:設置永久代大小為16m。
-XX:MaxTenuringThreshold=0:設置垃圾最大年齡。如果設置為0的話,則年輕代對象不經過Survivor區,直接進入老年代。對於老年代比較多的應用,可以提高效率。如果此值設置為一個較大值,則年輕代對象會在Survivor區進行多次復制,這樣可以增加對象在年輕代的存活時間,增加在年輕代被回收的概率。
MaxTenuringThreshold這個參數用於控制對象能經歷多少次Minor GC才晉升到老年代,默認值是15
按代的垃圾回收機制
新生代(Young generation):絕大多數最新被創建的對象都會被分配到這里,由於大部分在創建后很快變得不可達,很多對象被創建在新生代,然后“消失”。對象從這個區域“消失”的過程我們稱之為:Minor GC 。
老年代(Old generation):對象沒有變得不可達,並且從新生代周期中存活了下來,會被拷貝到這里。其區域分配的空間要比新生代多。也正由於其相對大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代中消失的過程,稱之為:Major GC 或者 Full GC。
持久代(Permanent generation)也稱之為 方法區(Method area):用於保存類常量以及字符串常量。注意,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算為 Major GC 。只不過在這個區域發生GC的條件非常嚴苛,必須符合以下三種條件才會被回收:
1、所有實例被回收
2、加載該類的ClassLoader 被回收
3、Class 對象無法通過任何途徑訪問(包括反射)
如果老年代的對象需要引用新生代的對象,會發生什么呢?
為了解決這個問題,老年代中存在一個 card table ,它是一個512byte大小的塊。所有老年代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只需要查詢 card table 來決定是否可以被回收,而不用查詢整個老年代。這個 card table 由一個write barrier 來管理。write barrier給GC帶來了很大的性能提升,雖然由此可能帶來一些開銷,但完全是值得的。
默認的新生代(Young generation)、老年代(Old generation)所占空間比例為 1 : 2
新生代空間的構成與邏輯
1) 一個伊甸園空間(Eden)
2) 兩個幸存者空間(Fron Survivor、To Survivor)
默認新生代空間的分配:Eden : From : To = 8 : 1 : 1
剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;最初一次,當Eden區滿的時候,執行Minor GC,將消亡(不可達)的對象清理掉,並將剩余(存活)的對象復制到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);下次Eden區滿了,再執行一次Minor GC,將消亡的對象清理掉,將存活的對象復制到Survivor1中,然后清空Eden區;將Survivor0中消亡的對象清理掉,將其中可以晉級的對象晉級到Old區,將存活的對象也復制到Survivor1區,然后清空Survivor0區;當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不代表一定是這個值)之后,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被復制到老年代。
- 新生代:
- 大多數新生的對象在Eden區分配,當Eden區沒有足夠空間進行分配時,虛擬機就會進行一次MinorGC。
- 在方法中new一個對象,方法調用完畢,對象就無用,這就是典型的新生代對象。(新生對象在Eden區經歷過一次MinorGC並且被Survivor容納的話,對象年齡為1,並且每熬過一次MinorGC,年齡就會加1,直到15,就會晉升到老年代)
- 注意動態對象的判定:Survivor空間中相同年齡的對象大小總和大於Survivor空間的一半,大於或者等於該年齡的對象就可以直接進入老年代。
- 老年代:
- 在新生代中經歷了N次垃圾回收后仍然存活的對象,就會被放到老年代中,而且大對象(占用大量連續內存空間的java對象如很長的字符串及數組)直接進入老年代。
- 當survivor空間不夠用時,需要依賴老年代進行分配擔保。
- 永久代:
- 方法區
- 主要存放Class和Meta的信息,Class在被加載的時候被放入永久代。 它和存放對象的堆區域不同,GC(Garbage Collection)不會在主程序運行期對永久代進行清理,所以如果你的應用程序會加載很多Class的話,就很可能出現PermGen space錯誤。
GC分類
- MinorGC:是指清理新生代
- MajorGC:是指清理老年代(很多MajorGC是由MinorGC觸發的)
- FullGC:是指清理整個堆空間包括年輕代和永久代
JVM GC 算法講解
1、根搜索算法
根搜索算法是從離散數學中的圖論引入的,程序把所有引用關系看作一張圖,從一個節點GC ROOT 開始,尋找對應的引用節點,找到這個節點后,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。
目前Java中可以作為GC ROOT的對象有:
1、Java虛擬機棧中引用的對象(棧幀中的本地變量表)
2、方法區中靜態屬性引用的對象
3、方法區中常量引用的對象
4、本地方法棧中JNI本地方法引用的對象(Native對象)
2、標記 - 清除算法
標記-清除算法采用從根集合進行掃描,對存活的對象進行標記,標記完畢后,再掃描整個空間中未被標記的對象進行直接回收。
標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活的對象比較多的情況下極為高效,但由於標記-清除算法直接回收不存活的對象,並沒有對還存活的對象進行整理,因此會導致內存碎片。
3、復制算法
復制算法將內存划分為兩個區間,使用此算法時,所有動態分配的對象都只能分配在其中一個區間(活動區間),而另外一個區間(空間區間)則是空閑的。
復制算法在存活對象比較少的時候,極為高效,但是帶來的成本是犧牲一半的內存空間用於進行對象的移動。
4、標記 - 整理算法
標記-整理算法采用 標記-清除 算法一樣的方式進行對象的標記、清除,但在回收不存活的對象占用的空間后,會將所有存活的對象往左端空閑空間移動,並更新對應的指針。標記-整理 算法是在標記-清除 算法之上,又進行了對象的移動排序整理,因此成本更高,但卻解決了內存碎片的問題。
垃圾回收器簡介
1、Serial(-XX:+UseSerialGC)
從名字我們可以看出,這是一個串行收集器。Serial收集器是Java虛擬機中最基本、歷史最悠久的收集器。在JDK1.3之前是Java虛擬機新生代收集器的唯一選擇。目前也是ClientVM下ServerVM 4核4GB以下機器默認垃圾回收器。Serial收集器並不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需暫停所有的用戶線程,直到回收結束。
使用算法:復制算法
2、SerialOld(-XX:+UseSerialGC)
SerialOld是Serial收集器的老年代收集器版本,它同樣是一個單線程收集器,這個收集器目前主要用於Client模式下使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的后備預案,如果CMS出現Concurrent Mode Failure,則SerialOld將作為后備收集器。
使用算法:標記 - 整理算法
3、ParNew(-XX:+UseParNewGC)
ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,只有它能與CMS收集器配合工作。
使用算法:復制算法
4、ParallelScavenge(-XX:+UseParallelGC)
ParallelScavenge又被稱為吞吐量優先收集器,和ParNew 收集器類似,是一個新生代收集器。
使用算法:復制算法
ParallelScavenge收集器的目標是達到一個可控件的吞吐量,所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。如果虛擬機總共運行了100分鍾,其中垃圾收集花了1分鍾,那么吞吐量就是99% 。
5、ParallelOld(-XX:+UseParallelOldGC)
ParallelOld是並行收集器,和SerialOld一樣,ParallelOld是一個老年代收集器,是老年代吞吐量優先的一個收集器。這個收集器在JDK1.6之后才開始提供的,在此之前,ParallelScavenge只能選擇SerialOld來作為其老年代的收集器,這嚴重拖累了ParallelScavenge整體的速度。而ParallelOld的出現后,“吞吐量優先”收集器才名副其實!
使用算法:標記 - 整理算法
6、CMS (-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark-Sweep)是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器,是一個老年代收集器,全稱 Concurrent Low Pause Collector,是JDK1.4后期開始引用的新GC收集器,在JDK1.5、1.6中得到了進一步的改進。它是對於響應時間的重要性需求大於吞吐量要求的收集器。對於要求服務器響應速度高的情況下,使用CMS非常合適。
CMS的一大特點,就是用兩次短暫的暫停來代替串行或並行標記整理算法時候的長暫停。
使用算法:標記 - 整理算法
7、GarbageFirst(G1)
這是一個新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了這個回收器,Sun公司預期SunHotSpot1.7發布正式版本。通過重新划分內存區域,整合優化CMS,同時注重吞吐量和響應時間。杯具的是Oracle收購這個收集器之后將其用於商用收費版收集器。因此目前暫時沒有發現哪個公司使用它,這個放在之后再去研究吧。
新生代收集器:
Serial (-XX:+UseSerialGC)
ParNew(-XX:+UseParNewGC)
ParallelScavenge(-XX:+UseParallelGC)
G1 收集器
老年代收集器:
SerialOld(-XX:+UseSerialOldGC)
ParallelOld(-XX:+UseParallelOldGC)
CMS(-XX:+UseConcMarkSweepGC)
G1 收集器
CMS使用場景:GC過程短暫停,適合對時延要求較高的服務,用戶線程不允許長時間的停頓
觸發條件
1、如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,虛擬機會根據收集的數據決定是否觸發(建議線上環境帶上這個參數,不然會加大問題排查的難度)。
2、老年代使用率達到閾值 CMSInitiatingOccupancyFraction
,默認92%。
3、永久代的使用率達到閾值 CMSInitiatingPermOccupancyFraction
,默認92%,前提是開啟 CMSClassUnloadingEnabled
。
4、新生代的晉升擔保失敗。
晉升擔保失敗
老年代是否有足夠的空間來容納全部的新生代對象或歷史平均晉升到老年代的對象,如果不夠的話,就提早進行一次老年代的回收,防止下次進行YGC的時候發生晉升失敗。
CMS的執行過程如下:
· 初始標記(STW initial mark)
在這個階段,需要虛擬機停頓正在執行的應用線程,官方的叫法STW(Stop Tow World)。這個過程從根對象掃描直接關聯的對象,並作標記。這個過程會很快的完成。
· 並發標記(Concurrent marking)
這個階段緊隨初始標記階段,在“初始標記”的基礎上繼續向下追溯標記。注意這里是並發標記,表示用戶線程可以和GC線程一起並發執行,這個階段不會暫停用戶的線程哦。
· 並發預清理(Concurrent precleaning)
這個階段依舊是並發的,JVM查找正在執行“並發標記”階段時候進入老年代的對象(可能這時會有對象從新生代晉升到老年代,或被分配到老年代)。通過重新掃描,減少在一個階段“重新標記”的工作,因為下一階段會STW。
· 重新標記(STW remark)
這個階段會再次暫停正在執行的應用線程,重新從根對象開始查找並標記並發階段遺漏的對象(在並發標記階段結束后對象狀態的更新導致),並處理對象關聯。這一次耗時會比“初始標記”更長,並且這個階段可以並行標記。
· 並發清理(Concurrent sweeping)
這個階段是並發的,應用線程和GC清除線程可以一起並發執行。
· 並發重置(Concurrent reset)
這個階段依舊是並發的,重置CMS收集器的數據結構,等待下一次垃圾回收。
CMS的缺點:
1、內存碎片。由於使用了標記-清理 算法,導致內存空間中會產生內存碎片。不過CMS收集器做了一些小的優化,就是把未分配的空間匯總成一個列表,當有JVM需要分配內存空間的時候,會搜索這個列表找到符合條件的空間來存儲這個對象。但是內存碎片的問題依然存在,如果一個對象需要3塊連續的空間來存儲,因為內存碎片的原因,尋找不到這樣的空間,就會導致Full GC。
2、需要更多的CPU資源。由於使用了並發處理,很多情況下都是GC線程和應用線程並發執行的,這樣就需要占用更多的CPU資源,也是犧牲了一定吞吐量的原因。
3、需要更大的堆空間。因為CMS標記階段應用程序的線程還是執行的,那么就會有堆空間繼續分配的問題,為了保障CMS在回收堆空間之前還有空間分配給新加入的對象,必須預留一部分空間。CMS默認在老年代空間使用92%時候啟動垃圾回收。可以通過-XX:CMSinitiatingOccupancyFraction=n來設置這個閥值。