1. 程式人生 > >JVM記憶體結構--新生代及新生代裡的兩個Survivor區(下一輪S0與S1交換角色,如此迴圈往復)、常見調優引數

JVM記憶體結構--新生代及新生代裡的兩個Survivor區(下一輪S0與S1交換角色,如此迴圈往復)、常見調優引數

轉自http://www.cnblogs.com/duanxz/p/6076662.html

一、為什麼會有年輕代

  我們先來屢屢,為什麼需要把堆分代?不分代不能完成他所做的事情麼?其實不分代完全可以,分代的唯一理由就是優化GC效能。你先想想,如果沒有分代,那我們所有的物件都在一塊,GC的時候我們要找到哪些物件沒用,這樣就會對堆的所有區域進行掃描。而我們的很多物件都是朝生夕死的,如果分代的話,我們把新建立的物件放到某一地方,當GC的時候先把這塊存“朝生夕死”物件的區域進行回收,這樣就會騰出很大的空間出來。

二.年輕代中的GC

  新生代大小(PSYoungGen total 9216K)=eden大小(eden space 8192K)+1個survivor大小(from space 1024K)

  HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。預設比例為8(Eden):1(一個survivor),為啥預設會是這個比例,接下來我們會聊到。一般情況下,新建立的物件都會被分配到Eden區(一些大物件特殊處理),這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。物件在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。
  因為年輕代中的物件基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收演算法使用的是複製演算法,複製演算法的基本思想就是將記憶體分為兩塊,每次只用其中一塊,當這一塊記憶體用完,就將還活著的物件複製到另外一塊上面。複製演算法不會產生記憶體碎片。
在GC開始的時候,物件只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到“To”,而在“From”區中,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色

,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有物件移動到年老代中。

三、一個物件的這一輩子

  我是一個普通的Java物件,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代裡,人很多,並且年齡都挺大的,我在這裡也認識了很多人。在年老代裡,我生活了20年(每次GC加一歲),然後被回收。

四、為什麼要有Survivor區

先不去想為什麼有兩個Survivor區,第一個問題是,設定Survivor區的意義在哪裡? 

如果沒有Survivor,Eden區每進行一次Minor GC,存活的物件就會被送到老年代。老年代很快被填滿,觸發Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發了Full GC)。老年代的記憶體空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多。你也許會問,執行時間長有什麼壞處?頻發的Full GC消耗的時間是非常可觀的,這一點會影響大型程式的執行和響應速度,更不要說某些連線會因為超時發生連線錯誤了。

好,那我們來想想在沒有Survivor的情況下,有沒有什麼解決辦法,可以避免上述情況:

方案 優點 缺點
增加老年代空間 更多存活物件才能填滿老年代。降低Full GC頻率 隨著老年代空間加大,一旦發生Full GC,執行所需要的時間更長
減少老年代空間 Full GC所需時間減少 老年代很快被存活物件填滿,Full GC頻率增加

顯而易見,沒有Survivor的話,上述兩種解決方案都不能從根本上解決問題。

我們可以得到第一條結論:Survivor的存在意義,就是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的物件,才會被送到老年代。

五、為什麼要設定兩個Survivor區

設定兩個Survivor區最大的好處就是解決了碎片化,下面我們來分析一下。

為什麼一個Survivor區不行?第一部分中,我們知道了必須設定Survivor區。假設現在只有一個survivor區,我們來模擬一下流程: 
剛剛新建的物件在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活物件就會被移動到Survivor區。這樣繼續迴圈下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活物件,如果此時把Eden區的存活物件硬放到Survivor區,很明顯這兩部分物件所佔有的記憶體是不連續的,也就導致了記憶體碎片化。 
我繪製了一幅圖來表明這個過程。其中色塊代表物件,白色框分別代表Eden區(大)和Survivor區(小)。Eden區理所當然大一些,否則新建物件很快就導致Eden區滿,進而觸發Minor GC,有悖於初衷。 

碎片化帶來的風險是極大的,嚴重影響Java程式的效能。堆空間被散佈的物件佔據不連續的記憶體,最直接的結果就是,堆中沒有足夠大的連續記憶體空間,接下去如果程式需要給一個記憶體需求很大的物件分配記憶體。。。畫面太美不敢看。。。這就好比我們爬山的時候,揹包裡所有東西緊挨著放,最後就可能省出一塊完整的空間放相機。如果每件行李之間隔一點空隙亂放,很可能最後就要一路把相機掛在脖子上了。

那麼,順理成章的,應該建立兩塊Survivor區,剛剛新建的物件在Eden中,經歷一次Minor GC,Eden中的存活物件就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活物件又會被複制送入第二塊survivor space S1(這個過程非常重要,因為這種複製演算法保證了S1中來自S0和Eden兩部分的存活物件佔用連續的記憶體空間,避免了碎片化的發生)。S0和Eden被清空,然後下一輪S0與S1交換角色,如此迴圈往復。如果物件的複製次數達到16次,該物件就會被送到老年代中。下圖中每部分的意義和上一張圖一樣,就不加註釋了。 

上述機制最大的好處就是,整個過程中,永遠有一個survivor space是空的,另一個非空的survivor space無碎片

那麼,Survivor為什麼不分更多塊呢?比方說分成三個、四個、五個?顯然,如果Survivor區再細分下去,每一塊的空間就會比較小,很容易導致Survivor區滿,因此,我認為兩塊Survivor區是經過權衡之後的最佳方案。

六、有關年輕代的JVM引數

1)-XX:NewSize和-XX:MaxNewSize(jdk1.3or1.4)

用於設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。

2)-Xmn(jdk1.4or lator)

用於設定年輕代大小。例如:-Xmn10m,設定新生代大小為10m。此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是(eden+1 survivor space)不同的。

3)-XX:SurvivorRatio

用於設定Eden和其中一個Survivor的比值,預設比例為8(Eden):1(一個survivor),這個值也比較重要。

例如:-XX:SurvivorRatio=4:設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區佔整個年輕代的1/6。

例子:-XX:SurvivorRatio=8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10。

複製程式碼
package com.jvm.study.part3;

import java.util.concurrent.TimeUnit;

public class GCTest {

    private static final int _1MB = 1024 * 1024;
    
    /**
     * @VM args:-verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8
     */
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        System.out.println("1");
        allocation2 = new byte[2 * _1MB];
        System.out.println("2");
        allocation3 = new byte[2 * _1MB];
        System.out.println("3");
        allocation4 = new byte[2 * _1MB];
        System.out.println("4");
        
    }
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(30);
        testAllocation();
    }

}
複製程式碼

結果:

複製程式碼
1
2
3
[GC (Allocation Failure) [PSYoungGen: 7136K->632K(9216K)] 7136K->6784K(19456K), 0.0084250 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 632K->0K(9216K)] [ParOldGen: 6152K->6635K(10240K)] 6784K->6635K(19456K), [Metaspace: 2562K->2562K(1056768K)], 0.0092126 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
4
Heap
 PSYoungGen      total 9216K, used 2290K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff83c960,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 6635K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 64% used [0x00000000fec00000,0x00000000ff27afa8,0x00000000ff600000)
 Metaspace       used 2569K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 277K, capacity 386K, committed 512K, reserved 1048576K
複製程式碼

結果分析:

1、-Xmn=10m,但實際的新生代大小(PSYoungGen total 9216K)=eden大小(eden space 8192K)+1個survivor大小(from space 1024K)

4)-XX:+PrintTenuringDistribution

這個引數用於顯示每次Minor GC時Survivor區中各個年齡段的物件的大小。


5).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用於設定晉升到老年代的物件年齡的最小值和最大值,每個物件在堅持過一次Minor GC之後,年齡就加1。