Java虚拟机7:内存分配原则
前言
對象的內存分配,往大的方向上講,就是在堆上分配,少數情況下也可能會直接分配在老年代中,分配的規則并不是百分之百固定的,其細節決定于當前使用的是哪種垃圾收集器組合,當然還有虛擬機中與內存相關的參數。垃圾收集器組合一般就是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] Heapdef 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模式下
HeapPSYoungGen 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] HeapPSYoungGen 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模式下
Heapdef 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模式下
HeapPSYoungGen 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”設置要求的年齡
總結
以上是生活随笔為你收集整理的Java虚拟机7:内存分配原则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Toast的基本用法 吐司打印
- 下一篇: 数据库连接池DBPool分析(一):简介