JVM 调优和垃圾回收器说明
轉載自? ?JVM 調優和垃圾回收器說明
JVM垃圾收集算法
? ? ? JVM垃圾收集算法有四種:標記-清除算法、復制算法、標記-整理算法、分代收集算法
? ? ??標記-清除算法:
? ? ? 該算法如同它的名字一樣,分為兩個階段:標記、清除。首先標記出所有需要回收的對象,然后,統一清除這些被標記的對象。該算法的缺點是:1、效率不高;2、產生大量不連續的內存碎片,導致有大量內存剩余的情況下,由于,沒有連續的空間來存放較大的對象,從而觸發了另一次垃圾收集動作。
? ? ? 復制算法:
? ? ? 由于標記-清除算法的效率不高,從而提出了復制算法。復制算法將可用的內存分成兩樣大小的兩塊,每次只使用其中一塊內存。當這塊內存用完之后,就把還存活的對象復制到另外一塊上面,然后,把這塊清空。復制算法克服了標記-清除算法的兩個缺點,但是太浪費內存,相當于內存空間減小了一半。
? ? ? 隨著時間的積累,現在使用的復制算法的虛擬機,不再是把內存分為1:1的兩塊。因為98%的對象是壽命很短的,創建之后,很快就被回收了,存活下來的只有2%,所以,用來存儲存活對象的內存區,可以小一些?,F在的商業虛擬機是把可用內存分為一個較大的Eden空間和兩個較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,把Eden和Survivor中的存活對象一次復制到另一塊Survivor內存區上,然后把Eden和剛才用過的Survivor空間清空。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,這樣,每次新產生的對象可以使用90%的內存空間。
? ? ? 標記-整理算法
? ? ? 從名字可以看出,該算法是對“標記-清除”算法的改進升級版。同樣的該算法分為兩個階段:標記、整理。標記階段同“標記-清除”算法。整理階段,不是直接對標記對象進行清理,而是讓所有存活的對象都移動到一端,然后,直接把邊界以外的內存清空。這就解決了“標記-清除”算法會造成大量不連續內存碎片的問題。
? ? ? 分代收集算法
? ? ? 分代收集算法是根據對象的存活周期的不同,將內存劃分為幾塊。當前的商業虛擬機的垃圾收集都采用了該算法。一般把Java堆分成新生代(年輕代)和老年代(年老代)。這樣就可以根據各年代中對象的存活周期來選擇最合適的收集算法了。新生代,由于只有少量的對象能存活下來,所以選用“復制算法”,只需要付出少量存活對象的復制成本。老年代,由于對象的存活率高,沒有額外的空間分擔,就必須使用“標記-清除”或“標記-整理”算法。
?
JVM垃圾收集器
? ? ? 由于內存中的對象,是按存活周期存放在不同的內存塊中的,所以,我們選擇不同的算法來針對不同的內存塊進行垃圾收集。從而,對于,不同的內存塊,我們需要有不同的垃圾收集器。
? ? ? 新生代的垃圾收集器有:Serial收集器、ParNew收集器、Parallel Scavenge收集器
? ? ? 老年代的垃圾收集器有:Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器
? ? ??
? ? ? 下面我們來分別介紹一下這些垃圾收集器
? ? ? Serial收集器/Serial Old收集器
? ? ? Serial收集器/Serial Old收集器,是單線程的,使用“復制”算法。當它工作時,必須暫停其它所有工作線程。特點:簡單而高效。對于運行在Client模式下的虛擬機來說是一個很好的選擇。
? ? ? ParNew收集器
? ? ? ParNew收集器,是Serial收集器的多線程版。是運行在Server模式下的虛擬機中首選的新生代收集器。除了Serial收集器外,目前只有它能與CMS收集器配合工作。
? ? ? Parallel Scavenge收集器/Parallel Old收集器
? ? ? Parallel Scavenge收集器,也是使用“復制”算法的、并行的多線程收集器。這些都和ParNew收集器一樣。但它關注的是吞吐量(CPU用于運行用戶代碼的時間與CPU總消耗時間的比值),而其它收集器(Serial/Serial Old、ParNew、CMS)關注的是垃圾收集時用戶線程的停頓時間。
? ? ? Parallel Old收集器是Parallel Scavenge收集器的老年代版本。
? ? ? CMS收集器
? ? ? CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,使用“標記-清除”算法。
? ? ? CMS收集器分4個步驟進行垃圾收集工作:
? ? ? 1、初始標記 ? 2、并發標記 ? 3、重新標記 ? 4、并發清除
? ? ? 其中“初始標記”、“重新標記”是需要暫停其它所有工作線程的。
? ? ? G1收集器
? ? ? G1(Garbage First)收集器,基于“標記-整理”算法,可以非常精確地控制停頓。
問題
當我們的代碼出現下面的情形時,該算法將無法適應
a)?????????ObjA.obj = ObjB
b)?????????ObjB.obj - ObjA
?????????????????這樣的代碼會產生如下引用情形?objA指向objB,而objB又指向objA,這樣當其他所有的引用都消失了之后,objA和objB還有一個相互的引用,也就是說兩個對象的引用計數器各為1,而實際上這兩個對象都已經沒有額外的引用,已經是垃圾了。
? ? ? ? ? ? ? ?
?
2、??????????????根搜索算法
???????????????????根搜索算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。
?
?
?
目前java中可作為GC Root的對象有
1、????虛擬機棧中引用的對象(本地變量表)
2、????方法區中靜態屬性引用的對象
3、????方法區中常量引用的對象
4、????本地方法棧中引用的對象(Native對象)
說了這么多,其實我們可以看到,所有的垃圾回收機制都是和引用相關的,那我們來具體的來看一下引用的分類,到底有哪些類型的引用?每種引用都是做什么的呢?
Java中存在四種引用,每種引用如下:
1、??強引用
只要引用存在,垃圾回收器永遠不會回收
Object obj = new Object();
//可直接通過obj取得對應的對象?如obj.equels(new Object());
而這樣?obj對象對后面new Object的一個強引用,只有當obj這個引用被釋放之后,對象才會被釋放掉,這也是我們經常所用到的編碼形式。
2、??軟引用
非必須引用,內存溢出之前進行回收,可以通過以下代碼實現
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有時候會返回null
這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然,當這個對象被標記為需要回收的對象時,則返回null;
軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。
3、??弱引用
第二次垃圾回收時回收,可以通過如下代碼實現
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有時候會返回null
wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾
弱引用是在第二次垃圾回收時回收,短時間內通過弱引用取對應的數據,可以取到,當執行過第二次垃圾回收時,將返回null。
弱引用主要用于監控對象是否已經被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器
4、??虛引用(幽靈/幻影引用)
???????????垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回從內存中已經刪除
虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數據為null,因此也被成為幽靈引用。
虛引用主要用于檢測對象是否已經從內存中刪除。
在上文中已經提到了,我們的對象在內存中會被劃分為5塊區域,而每塊數據的回收比例是不同的,根據IBM的統計,數據如下圖所示:
?
?我們知道,方法區主要存放類與類之間關系的數據,而這部分數據被加載到內存之后,基本上是不會發生變更的,
Java堆中的數據基本上是朝生夕死的,我們用完之后要馬上回收的,而Java棧和本地方法棧中的數據,因為有后進先出的原則,當我取下面的數據之前,必須要把棧頂的元素出棧,因此回收率可認為是100%;而程序計數器我們前面也已經提到,主要用戶記錄線程執行的行號等一些信息,這塊區域也是被認為是唯一一塊不會內存溢出的區域。在SunHostSpot的虛擬機中,對于程序計數器是不回收的,而方法區的數據因為回收率非常小,而成本又比較高,一般認為是“性價比”非常差的,所以Sun自己的虛擬機HotSpot中是不回收的!但是在現在高性能分布式J2EE的系統中,我們大量用到了反射、動態代理、CGLIB、JSP和OSGI等,這些類頻繁的調用自定義類加載器,都需要動態的加載和卸載了,以保證永久帶不會溢出,他們通過自定義的類加載器進行了各項操作,因此在實際的應用開發中,類也是被經常加載和卸載的,方法區也是會被回收的!但是方法區的回收條件非常苛刻,只有同時滿足以下三個條件才會被回收!
?
1、所有實例被回收
2、加載該類的ClassLoader被回收
3、Class對象無法通過任何途徑訪問(包括反射)
好了,我們現在切入正題,Java1.2之前主要通過引用計數器來標記是否需要垃圾回收,而1.2之后都使用根搜索算法來收集垃圾,而收集后的垃圾是通過什么算法來回收的呢?
1、????標記-清除算法
2、????復制算法
3、????標記-整理算法
我們來逐一過一下
1、????標記-清除算法
?
?
標記-清除算法采用從根集合進行掃描,對存活的對象對象標記,標記完畢后,再掃描整個空間中未被標記的對象,進行回收,如上圖所示。
標記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由于標記-清除算法直接回收不存活的對象,因此會造成內存碎片!
2、????復制算法
??
?復制算法采用從根集合掃描,并將存活對象復制到一塊新的,沒有使用過的空間中,這種算法當控件存活的對象比較少時,極為高效,但是帶來的成本是需要一塊內存交換空間用于進行對象的移動。也就是我們前面提到的
s0 s1等空間。
?
3、????標記-整理算法
???
?標記
-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動,并更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。
?
我們知道,JVM為了優化內存的回收,進行了分代回收的方式,對于新生代內存的回收(minor GC)主要采用復制算法,下圖展示了minor GC的執行過程。
?
??
?對于新生代和舊生代,
JVM可使用很多種垃圾回收器進行垃圾回收,下圖展示了不同生代不通垃圾回收器,其中兩個回收器之間有連線表示這兩個回收器可以同時使用。
?
?而這些垃圾回收器又分為串行回收方式、并行回收方式合并發回收方式執行,分別運用于不同的場景。如下圖所示
?
?下面我們來逐一介紹一下每個垃圾回收器。
?
1、????Serial收集器
看名字我們都可以看的出來,這個屬于串行收集器。其運行示意圖如下
?Serial
收集器是歷史最悠久的一個回收器,JDK1.3之前廣泛使用這個收集器,目前也是ClientVM下?ServerVM 4核4GB以下機器的默認垃圾回收器。串行收集器并不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需要中斷所有的用戶線程,知道它回收結束為止,因此又號稱“Stop The World”?的垃圾回收器。注意,JVM中文名稱為java虛擬機,因此它就像一臺虛擬的電腦一樣在工作,而其中的每一個線程就被認為是JVM的一個處理器,因此大家看到圖中的CPU0、CPU1實際為用戶的線程,而不是真正機器的CPU,大家不要誤解哦。
?
串行回收方式適合低端機器,是Client模式下的默認收集器,對CPU和內存的消耗不高,適合用戶交互比較少,后臺任務較多的系統。
Serial收集器默認新舊生代的回收器搭配為Serial+ SerialOld
2、????ParNew收集器
ParNew收集器其實就是多線程版本的Serial收集器,其運行示意圖如下
?同樣有
Stop The World的問題,他是多CPU模式下的首選回收器(該回收器在單CPU的環境下回收效率遠遠低于Serial收集器,所以一定要注意場景哦),也是Server模式下的默認收集器。
?
3、????ParallelScavenge
ParallelScavenge又被稱為是吞吐量優先的收集器,器運行示意圖如下
?
?ParallelScavenge
所提到的吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鐘,JVM的垃圾回收占用1分鐘,那么吞吐量就是99%。在當今網絡告訴發達的今天,良好的響應速度是提升用戶體驗的一個重要指標,多核并行云計算的發展要求程序盡可能的使用CPU和內存資源,盡快的計算出最終結果,因此在交互不多的云端,比較適合使用該回收器。
?
4、????ParallelOld
ParallelOld是老生代并行收集器的一種,使用標記整理算法、是老生代吞吐量優先的一個收集器。這個收集器是JDK1.6之后剛引入的一款收集器,我們看之前那個圖之間的關聯關系可以看到,早期沒有ParallelOld之前,吞吐量優先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量優先的性能,自從JDK1.6之后,才能真正做到較高效率的吞吐量優先。其運行示意圖如下
???
?5、?SerialOld
SerialOld是舊生代Client模式下的默認收集器,單線程執行;在JDK1.6之前也是ParallelScvenge回收新生代模式下舊生代的默認收集器,同時也是并發收集器CMS回收失敗后的備用收集器。其運行示意圖如下
?
?6、?CMS
CMS又稱響應時間優先(最短回收停頓)的回收器,使用并發模式回收垃圾,使用標記-清除算法,CMS對CPU是非常敏感的,它的回收線程數=(CPU+3)/4,因此當CPU是2核的實惠,回收線程將占用的CPU資源的50%,而當CPU核心數為4時僅占用25%。他的運行示意圖如下
?
?CMS
模式主要分為4個過程
?在初始標記的時候,需要中斷所有用戶線程,在并發標記階段,用戶線程和標記線程
并發執行,而在這個過程中,隨著內存引用關系的變化,可能會發生原來標記的對象被釋放,進而引發新的垃圾,因此可能會產生一系列的浮動垃圾,不能被回收。
?
CMS?為了確保能夠掃描到所有的對象,避免在Initial Marking?中還有未標識到的對象,采用的方法為找到標記了的對象,并將這些對象放入Stack?中,掃描時尋找此對象依賴的對象,如果依賴的對象的地址在其之前,則將此對象進行標記,并同時放入Stack?中,如依賴的對象地址在其之后,則僅標記該對象。
在進行Concurrent Marking?時minor GC?也可能會同時進行,這個時候很容易造成舊生代對象引用關系改變,CMS?為了應對這樣的并發現象,提供了一個Mod Union Table?來進行記錄,在這個Mod Union Table中記錄每次minor GC?后修改了的Card?的信息。這也是ParallelScavenge不能和CMS一起使用的原因。
CMS產生浮動垃圾的情況請見如下示意圖
?
在運行回收過后,c就變成了浮動垃圾。
由于CMS會產生浮動垃圾,當回收過后,浮動垃圾如果產生過多,同時因為使用標記-清除算法會產生碎片,可能會導致回收過后的連續空間仍然不能容納新生代移動過來或者新創建的大資源,因此會導致CMS回收失敗,進而觸發另外一次FULL GC,而這時候則采用SerialOld進行二次回收。
同時CMS因為可能產生浮動垃圾,而CMS在執行回收的同時新生代也有可能在進行回收操作,為了保證舊生代能夠存放新生代轉移過來的數據,CMS在舊生代內存到達全部容量的68%就觸發了CMS的回收!
7、????GarbageFirst(G1 )
我們再來看垃圾回收器的總圖,剛才我們可以看到,我在圖上標記了一個?,其實這是一個新的垃圾回收器,既可以回收新生代也可以回收舊生代,SunHotSpot 1.6u14以上EarlyAccess版本加入了這個回收器,sun公司預期SunHotSpot1.7發布正式版,他是商用高性能垃圾回收器,通過重新劃分內存區域,整合優化CMS,同時注重吞吐量和響應時間,但是杯具的是被oracle收購之后這個收集器屬于商用收費收集器,因此目前基本上沒有人使用,我們在這里也就不多介紹,更多信息可以參考oracle新版本JDK說明。
下面我們再來看下JVM的一些內存分配與回收策略
1、????優先在Edon上分配對象
代碼示例
package com.yhj.jvm.gc.edenFirst;/*** @Described:Edon優先劃分對象測試* VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc* Edon s0 s1 old* 8 1 1 10* @author YHJ create at 2012-1-3 下午04:44:43* @FileNmae com.yhj.jvm.gc.edenFirst.EdonFirst.java*/public class EdonFirst {private final static int ONE_MB = 1024*1024;/*** @param args* @Author YHJ create at 2012-1-3 下午04:44:38*/public static void main(String[] args) {@SuppressWarnings("unused")byte[] testCase1,testCase2,testCase3,testCase4;testCase1 = new byte[2*ONE_MB];testCase2 = new byte[2*ONE_MB];testCase3 = new byte[2*ONE_MB];// testCase1 = null;// testCase2 = null;// testCase3 = null;testCase4 = new byte[2*ONE_MB];}}運行結果
?
?結果分析
從運行結果我們可以很清晰的看到,eden有8MB的存儲控件(通過參數配置),前6MB的數據優先分配到eden區域,當下一個2MB存放時,因空間已滿,觸發一次GC,但是這部分數據因為沒有回收(引用還在,當賦值為null后則不會轉移),數據會被復制到s0區域,但是s0區域不夠存儲,因此直接放入老生代區域,新的2MB數據存放在eden區域
2、????大對象直接進入老生代
代碼示例
運行結果
?結果分析
從代碼中我們可以看到,當testCase1劃分為0.25MB數據,進行多次大對象創建之后,testCase1應該在GC執行之后被復制到s0區域(s0足以容納testCase1),但是我們設置了對象的年齡為1,即超過1歲便進入老生代,因此GC執行2次后testCase1直接被復制到了老生代,而默認進入老生代的年齡為15。我們通過profilter的監控工具可以很清楚的看到對象的年齡,如圖所示
?
?
?右側的年代數目就是對象的年齡
?
4、????群體效應(大批中年對象進入老生代)
代碼示例
package com.yhj.jvm.gc.dynamicMoreAVG_intoOld;/*** @Described:s0占用空間到達50%直接進入老生代* VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=15-XX:+PrintGCDetails -verbose:gc* Edon s0 s1 old age* 8 1 1 10 15* 0.5 0 0 7.5* 7.5 0.5 0 7.5* 7.5 0 0 8* @author YHJ create at 2012-1-3 下午05:50:40* @FileNmae com.yhj.jvm.gc.dynamicMoreAVG_intoOld.MoreAVG_intoOld.java*/public class MoreAVG_intoOld {private final static int ONE_MB = 1024*1024;/*** @param args* @Author YHJ create at 2012-1-3 下午04:44:38*/public static void main(String[] args) {@SuppressWarnings("unused")byte[] testCase1,testCase2,testCase3,testCase4;testCase1 = new byte[7*ONE_MB+ONE_MB/2];testCase2 = new byte[ONE_MB/2];testCase3 = new byte[7*ONE_MB+ONE_MB/2];testCase3 = null;testCase4 = new byte[7*ONE_MB+ONE_MB/2];// testCase1 = new byte[7*ONE_MB+3*ONE_MB/4];// testCase2 = new byte[ONE_MB/4];// testCase3 = new byte[7*ONE_MB+3*ONE_MB/4];}}運行結果
?
?結果分析
我們看到,當創建后testCase3,testCase2被移動到s0區域,當被釋放后,繼續創建testCase3,按理說testCase2應該移動到s1區域,但是因為超過了s1區域的1/2,因此直接進入老生代
5、????擔保GC(擔保minorGC)
擔保GC就是擔保minorGC能夠滿足當前的存儲空間,而無需觸發老生代的回收,由于大部分對象都是朝生夕死的,因此,在實際開發中這種很起效,但是也有可能會發生擔保失敗的情況,當擔保失敗的時候會觸發FullGC,但是失敗畢竟是少數,因此這種一般是很劃算的。
?
代碼示例
package com.yhj.jvm.gc.securedTransactions;/*** @Described:擔保交易測試* VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:-HandlePromotionFailure 無擔保* VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:+HandlePromotionFailure 有擔保* Edon s0 s1 old * 8 1 1 10 * @author YHJ create at 2012-1-3 下午06:11:17* @FileNmaecom.yhj.jvm.gc.securedTransactions.SecuredTransactions.java*/public class SecuredTransactions {private final static int ONE_MB = 1024*1024;/*** @param args* @Author YHJ create at 2012-1-3 下午04:44:38*/public static void main(String[] args) {@SuppressWarnings("unused")byte[] testCase1,testCase2,testCase3,testCase4,testCase5,testCase6,testCase7;testCase1 = new byte[2*ONE_MB];testCase2 = new byte[2*ONE_MB];testCase3 = new byte[2*ONE_MB];testCase1 = null;testCase4 = new byte[2*ONE_MB];testCase5 = new byte[2*ONE_MB];testCase6 = new byte[2*ONE_MB];testCase4 = null;testCase5 = null;testCase6 = null;testCase7 = new byte[2*ONE_MB];}}?運行結果
1、??無擔保
? ??
?2、有擔保
?結果分析
我們可以很清楚的看到,當無擔保的時候,觸發了一次FullGC?而有擔保的情況下,只有monorGC則完成了回收,大大提升了效率。
當我們注釋掉對應的代碼
// testCase4 = null; // testCase5 = null; // testCase6 = null;?的時候,就會引發擔保失敗,如下圖所示
JVM默認情況是是開啟擔保的,無需設置參數。
總結
以上是生活随笔為你收集整理的JVM 调优和垃圾回收器说明的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java的GC机制及算法
- 下一篇: java io系列09之 FileDes