1. 程式人生 > >JVM調優(5)之分代

JVM調優(5)之分代

為什麼要分代

分代的垃圾回收策略,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高回收效率

堆記憶體是虛擬機器管理的記憶體中最大的一塊,也是垃圾回收最頻繁的一塊區域,我們程式所有的物件例項都存放在堆記憶體中。給堆記憶體分代是為了提高物件記憶體分配和垃圾回收的效率。試想一下,如果堆記憶體沒有區域劃分,所有的新建立的物件和生命週期很長的物件放在一起,隨著程式的執行,堆記憶體需要頻繁進行垃圾收集,而每次回收都要遍歷所有的物件,遍歷這些物件所花費的時間代價是巨大的,會嚴重影響我們的GC效率,這簡直太可怕了。

有了記憶體分代,情況就不同了,新建立的物件會在新生代中分配記憶體,經過多次回收仍然存活下來的物件存放在老年代中,靜態屬性、類資訊等存放在永久代中,新生代中的物件存活時間短,只需要在新生代區域中頻繁進行GC,老年代中物件生命週期長,記憶體回收的頻率相對較低,不需要頻繁進行回收,永久代中回收效果太差,一般不進行垃圾回收

,還可以根據不同年代的特點採用合適的垃圾收集演算法。分代收集大大提升了收集效率,這些都是記憶體分代帶來的好處。

如何分代

這裡寫圖片描述

Java虛擬機器將堆記憶體劃分為年輕代(Young Generation)年老代(Old Generation)持久代(Permanent Generation),持久代是HotSpot虛擬機器特有的概念,它採用持久代的方式來實現方法區,其他的虛擬機器實現沒有這一概念,而且HotSpot也有取消永久代的趨勢,在JDK 1.7中HotSpot已經開始了“去永久化”,把原本放在持久代的字串常量池移出。持久代主要存放java常量、類資訊、靜態變數等資料,與垃圾回收關係不大,新生代和年老代是垃圾回收的主要區域。記憶體分代示意圖如下
這裡寫圖片描述

年輕代(Young Generation)

新生成的物件優先存放在新生代中,新生代物件朝生夕死,存活率很低,在新生代中,常規應用進行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。

HotSpot將新生代劃分為三塊,一塊較大的Eden空間和兩塊較小的Survivor空間,預設比例為8:1:1。劃分的目的是因為HotSpot採用複製演算法來回收新生代,設定這個比例是為了充分利用記憶體空間,減少浪費。新生成的物件在Eden區分配(大物件除外,大物件直接進入老年代),當Eden區沒有足夠的空間進行分配時,虛擬機器將發起一次Minor GC。

1.所有新生成的物件首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。
2.新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。

一個Eden區,兩個 Survivor區(一般而言)。大部分物件在Eden區中生成。回收時先將eden區存活物件複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活物件複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。

3.當survivor1區不足以存放 eden和survivor0的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收

4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)

年老代(Old Generation)

在新生代中經歷了多次(具體看虛擬機器配置的閥值)GC後仍然存活下來的物件會進入老年代中。老年代中的物件生命週期較長,存活率比較高,在老年代中進行GC的頻率相對而言較低,而且回收的速度也比較慢。

1.在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

2.記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。

持久代(Permanent Generation)

用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate 等,在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。

什麼情況下觸發垃圾回收

Minor GC
一般情況下,當新物件生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出來。

Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

1.年老代(Tenured)被寫滿
2.持久代(Perm)被寫滿
3.System.gc()被顯示呼叫
4.上一次GC之後Heap的各域分配策略動態變化

選擇合適的垃圾收集演算法

序列收集器

這裡寫圖片描述
用單執行緒處理所有垃圾回收工作,因為無需多執行緒互動,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用-XX:+UseSerialGC開啟。

並行收集器

這裡寫圖片描述

並行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了增強–可以對年老代進行並行收集。
如果年老代不使用併發收集的話,預設是使用單執行緒進行垃圾回收,因此會制約擴充套件能力。使用-XX:+UseParallelOldGC開啟。
使用-XX:ParallelGCThreads=<N>設定並行垃圾回收的執行緒數。此值可以設定與機器處理器數量相等。
此收集器可以進行如下配置:

最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,通過-XX:MaxGCPauseMillis=<N>指定。<N>為毫秒.如果指定了此值的話,堆大小和垃圾回收相關引數會進行調整以達到指定值。設定此值可能會減少應用的吞吐量。

吞吐量:吞吐量為垃圾回收時間與非垃圾回收時間的比值,通過-XX:GCTimeRatio=<N>來設定,公式為1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。預設情況為99,即1%的時間用於垃圾回收。

併發收集器

這裡寫圖片描述
可以保證大部分工作都併發進行(應用不停止),垃圾回收只暫停很少的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC開啟。

併發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收執行緒,跟蹤可達物件。在每個年老代垃圾回收週期中,在收集初期併發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個執行緒同時進行垃圾回收工作。

併發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。

在只有一個處理器的主機上使用併發收集器,設定為incremental mode模式也可獲得較短的停頓時間。

浮動垃圾:由於在應用執行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收週期時才能回收掉。所以,併發收集器一般需要20%的預留空間用於這些浮動垃圾。
Concurrent Mode Failure:併發收集器在應用執行時進行收集,所以需要保證堆在垃圾回收的這段時間有足夠的空間供程式使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下將會發生“併發模式失敗”,此時整個應用將會暫停,進行垃圾回收。
啟動併發收集器:因為併發收集在應用執行時進行收集,所以必須保證收集完成之前有足夠的記憶體空間供程式使用,否則會出現“Concurrent Mode Failure”。通過設定-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少剩餘堆時開始執行併發收集

小結

序列處理器:

–適用情況:資料量比較小(100M左右);單處理器下並且對響應時間無要求的應用。
–缺點:只能用於小型應用,只有一個執行緒,執行垃圾回收時程式停止的時間比較長
–語法:

  1. -XX:+UseSerialGC
  2. 新生代、老年代使用序列回收
  3. 新生代複製演算法
  4. 老年代標記-壓縮

並行處理器:

–適用情況:“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:後臺處理、科學計算。
–缺點:垃圾收集過程中應用響應時間可能加長
–語法

  1. -XX:+UseParNewGC(新生代使用並行收集器,老年代使用序列回收收集器) 關鍵字(ParNew)
  2. -XX:+UseParallelGC(新生代使用並行回收收集器,老年代使用序列收集器)
  3. -XX:+UseParallelOldGC(新生代,老年代都使用並行回收收集器) 關鍵字(PSYoungGen)
  4. -XX:+UseConcMarkSweepGC(新生代使用並行收集器,老年代使用CMS)
  5. -XX:ParallelGCThreads 限制並行執行緒數量
  6. -XX:MaxGCPauseMills GC最大停頓毫秒數
  7. -XX:GCTimeRatio 垃圾回收佔用的CPU時間比例,預設99代表最大執行1%時間做GC
  8. 新生代複製演算法
  9. 老年代標記-壓縮

併發處理器:

–適用情況:“對響應時間有高要求”,多CPU、對應用響應時間有較高要求的中、大型應用。
舉例:Web伺服器/應用伺服器、電信交換、整合開發環境。
系統和垃圾回收一起執行,系統不會暫停
–語法

  1. -XX:+UseConcMarkSweepGC(設定年老代為併發收集,年輕代並行,適合於響應要求高的系統)

參考文件:
JVM_垃圾回收序列、並行、併發演算法(總結)
Java虛擬機器:JVM記憶體分代策略
JVM調優總結(六)-分代垃圾回收詳述