Java虛擬機器7:記憶體分配原則

五月的倉頡發表於2015-09-26

前言

物件的記憶體分配,往大的方向上講,就是在堆上分配,少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,其細節決定於當前使用的是哪種垃圾收集器組合,當然還有虛擬機器中與記憶體相關的引數。垃圾收集器組合一般就是Serial+Serial Old和Parallel+Serial Old,前者是Client模式下的預設垃圾收集器組合,後者是Server模式下的預設垃圾收集器組合,文章使用對比學習法對比Client模式下和Server模式下同一條物件分配原則有什麼區別。

 

TLAB

首先講講什麼是TLAB。記憶體分配的動作,可以按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB)。哪個執行緒需要分配記憶體,就在哪個執行緒的TLAB上分配。虛擬機器是否使用TLAB,可以通過-XX:+/-UseTLAB引數來設定。這麼做的目的之一,也是為了併發建立一個物件時,保證建立物件的執行緒安全性。TLAB比較小,直接在TLAB上分配記憶體的方式稱為快速分配方式,而TLAB大小不夠,導致記憶體被分配在Eden區的記憶體分配方式稱為慢速分配方式。

 

物件優先分配在Eden區上

上面講了不同的垃圾收集器組合對於記憶體分配規則是有影響的,看下影響在什麼地方並解釋一下原因,虛擬機器引數為“-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8”,即10M新生代,10M老年代,10M新生代中8M的Eden區,兩個Survivor區各1M。程式碼都是同一段

public class EdenAllocationTest
{
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[4 * _1MB];
    }
}

Client模式下

[GC [DefNew: 6487K->194K(9216K), 0.0042856 secs] 6487K->6338K(19456K), 0.0043281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4454K [0x0000000005180000, 0x0000000005b80000, 0x0000000005b80000)
  eden space 8192K,  52% used [0x0000000005180000, 0x00000000055a9018, 0x0000000005980000)
  from space 1024K,  18% used [0x0000000005a80000, 0x0000000005ab0810, 0x0000000005b80000)
  to   space 1024K,   0% used [0x0000000005980000, 0x0000000005980000, 0x0000000005a80000)
 tenured generation   total 10240K, used 6144K [0x0000000005b80000, 0x0000000006580000, 0x0000000006580000)
   the space 10240K,  60% used [0x0000000005b80000, 0x0000000006180048, 0x0000000006180200, 0x0000000006580000)
 compacting perm gen  total 21248K, used 2982K [0x0000000006580000, 0x0000000007a40000, 0x000000000b980000)
   the space 21248K,  14% used [0x0000000006580000, 0x0000000006869890, 0x0000000006869a00, 0x0000000007a40000)
No shared spaces configured.

Server模式下

Heap
 PSYoungGen      total 9216K, used 6651K [0x000000000af20000, 0x000000000b920000, 0x000000000b920000)
  eden space 8192K, 81% used [0x000000000af20000,0x000000000b59ef70,0x000000000b720000)
  from space 1024K, 0% used [0x000000000b820000,0x000000000b820000,0x000000000b920000)
  to   space 1024K, 0% used [0x000000000b720000,0x000000000b720000,0x000000000b820000)
 PSOldGen        total 10240K, used 4096K [0x000000000a520000, 0x000000000af20000, 0x000000000af20000)
  object space 10240K, 40% used [0x000000000a520000,0x000000000a920018,0x000000000af20000)
 PSPermGen       total 21248K, used 2972K [0x0000000005120000, 0x00000000065e0000, 0x000000000a520000)
  object space 21248K, 13% used [0x0000000005120000,0x0000000005407388,0x00000000065e0000)

看到在Client模式下,最後分配的4M在新生代中,先分配的6M在老年代中;在Server模式下,最後分配的4M在老年代中,先分配的6M在新生代中。說明不同的垃圾收集器組合對於物件的分配是有影響的。講下兩者差別的原因:

1、Client模式下,新生代分配了6M,虛擬機器在GC前有6487K,比6M也就是6144K多,多主要是因為TLAB和EdenAllocationTest這個物件佔的空間,TLAB可以通過“-XX:+PrintTLAB”這個虛擬機器引數來檢視大小。OK,6M多了,然後來了一個4M的,Eden+一個Survivor總共就9M不夠分配了,這時候就會觸發一次Minor GC。但是觸發Minor GC也沒用,因為allocation1、allocation2、allocation3三個引用還存在,另一塊1M的Survivor也不夠放下這6M,那麼這次Minor GC的效果其實是通過分配擔保機制將這6M的內容轉入老年代中。然後再來一個4M的,由於此時Minor GC之後新生代只剩下了194K了,夠分配了,所以4M順利進入新生代。

2、Server模式下,前面都一樣,但是在GC的時候有一點區別。在GC前還會進行一次判斷,如果要分配的記憶體>=Eden區大小的一半,那麼會直接把要分配的記憶體放入老年代中。要分配4M,Eden區8M,剛好一半,而且老年代10M,夠分配,所以4M就直接進入老年代去了。為了驗證一下結論,我們把3個2M之後分配的4M改為3M看一下

public class EdenAllocationTest
{
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[3 * _1MB];
    }
}

執行結果為

[GC [PSYoungGen: 6487K->352K(9216K)] 6487K->6496K(19456K), 0.0035661 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 352K->0K(9216K)] [PSOldGen: 6144K->6338K(10240K)] 6496K->6338K(19456K) [PSPermGen: 2941K->2941K(21248K)], 0.0035258 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 3236K [0x000000000af40000, 0x000000000b940000, 0x000000000b940000)
  eden space 8192K, 39% used [0x000000000af40000,0x000000000b269018,0x000000000b740000)
  from space 1024K, 0% used [0x000000000b740000,0x000000000b740000,0x000000000b840000)
  to   space 1024K, 0% used [0x000000000b840000,0x000000000b840000,0x000000000b940000)
 PSOldGen        total 10240K, used 6338K [0x000000000a540000, 0x000000000af40000, 0x000000000af40000)
  object space 10240K, 61% used [0x000000000a540000,0x000000000ab70858,0x000000000af40000)
 PSPermGen       total 21248K, used 2982K [0x0000000005140000, 0x0000000006600000, 0x000000000a540000)
  object space 21248K, 14% used [0x0000000005140000,0x0000000005429890,0x0000000006600000)

看到3M在新生代中,6M通過分配擔保機制進入老年代了。

 

大物件直接進入老年代

虛擬機器引數為“-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728”,最後那個參數列示大於這個設定值的物件直接在老年代中分配,這樣做的目的是為了避免在Eden區和兩個Survivor區之間發生大量的記憶體複製。測試程式碼為

public class OldTest
{
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args)
    {
         byte[] allocation = new byte[4 * _1MB];
    }
}

Client模式下

Heap
 def new generation   total 9216K, used 507K [0x0000000005140000, 0x0000000005b40000, 0x0000000005b40000)
  eden space 8192K,   6% used [0x0000000005140000, 0x00000000051bef28, 0x0000000005940000)
  from space 1024K,   0% used [0x0000000005940000, 0x0000000005940000, 0x0000000005a40000)
  to   space 1024K,   0% used [0x0000000005a40000, 0x0000000005a40000, 0x0000000005b40000)
 tenured generation   total 10240K, used 4096K [0x0000000005b40000, 0x0000000006540000, 0x0000000006540000)
   the space 10240K,  40% used [0x0000000005b40000, 0x0000000005f40018, 0x0000000005f40200, 0x0000000006540000)
 compacting perm gen  total 21248K, used 2972K [0x0000000006540000, 0x0000000007a00000, 0x000000000b940000)
   the space 21248K,  13% used [0x0000000006540000, 0x00000000068272a0, 0x0000000006827400, 0x0000000007a00000)
No shared spaces configured.

Server模式下

Heap
 PSYoungGen      total 9216K, used 4603K [0x000000000afc0000, 0x000000000b9c0000, 0x000000000b9c0000)
  eden space 8192K, 56% used [0x000000000afc0000,0x000000000b43ef40,0x000000000b7c0000)
  from space 1024K, 0% used [0x000000000b8c0000,0x000000000b8c0000,0x000000000b9c0000)
  to   space 1024K, 0% used [0x000000000b7c0000,0x000000000b7c0000,0x000000000b8c0000)
 PSOldGen        total 10240K, used 0K [0x000000000a5c0000, 0x000000000afc0000, 0x000000000afc0000)
  object space 10240K, 0% used [0x000000000a5c0000,0x000000000a5c0000,0x000000000afc0000)
 PSPermGen       total 21248K, used 2972K [0x00000000051c0000, 0x0000000006680000, 0x000000000a5c0000)
  object space 21248K, 13% used [0x00000000051c0000,0x00000000054a72a0,0x0000000006680000)

看到Client模式下4M直接進入了老年代,Server模式下4M還在新生代中。產生這個差別的原因是“-XX:PretenureSizeThreshold”這個引數對Serial+Serial Old垃圾收集器組合有效而對Parallel+Serial Old垃圾收集器組合無效

 

其他幾條原則

上面列舉的原則其實不重要,只是演示罷了,也不需要記住,因為實際過程中我們可能使用的並不是上面的垃圾收集器的組合,可能使用ParNew垃圾收集器,可能使用G1垃圾收集器。場景很多,重要的是要在實際使用的時候有辦法知道使用的垃圾收集器對於物件分配有哪些原則,因為理解這些原則才是調優的第一步。下面列舉一下物件分配的另外兩條原則:

1、長期存活的物件將進入老年代。Eden區中的物件在一次Minor GC後沒有被回收,則物件年齡+1,當物件年齡達到“-XX:MaxTenuringThreshold”設定的值的時候,物件就會被晉升到老年代中

2、Survivor空間中相同年齡的所有物件大小總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到“-XX:MaxTenuringThreshold”設定要求的年齡

相關文章