JVM内存分配

对象的内存分配,大致上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程缓冲,将按线程优先在TLAB(Thread-local allocation buffer)上分配。少数情况下也可能直接分配到老年代中,分配的规则并非固定,取决于使用的是哪一种垃圾收集器组合,还有内存相关的一些参数设置。
接下来是几条最普遍的内存分配规则,并通过代码去验证。验证的是在使用Serial/Serial Old(串行)收集器下的内存分配和回收。其他收集器组合不尽相同。

1、对象优先在Eden区分配

一般的情况下,对象在新生代的Eden去分配。当Eden区没有足够空间进行分配时,虚拟机将进行一次Minor GC。-XX:PrintGCDetails这个收集器日志参数,通知虚拟机在发生垃圾收集行为时打印内存日志,并且在进程退出时输出当前内存各区域分配情况。
代码实例1,testAllocation()运行时通过参数-Xms20M、-Xmx20M、-Xmn10M限制Java堆大小为20M,其中10M分配给新生代,剩余的10M给老年代。-XX:SurvivorRatio=8设置新生代中Eden区与一个Survivor的空间比例是8:1,因此新生代总可用空间为9M(Eden区的容量+1个Survivor的容量)。
testAllocation()尝试分配3个2M大小和1个4M大小的对象,再执行到分配allocation4对象的语句时会发生一次Minor GC。引起的原因是给allocation4分配内存时,Eden去已经被占用了6M,剩余空间已不足以分配allocation4所需的4M内存,因此发生Minor GC。这次GC的结果是新生代6635K变为177K,而Java堆的内存总占用量几乎没有减少,因为allocation1、allocation2、allocation3都是存活的,虚拟机几乎没有可回收的对象。GC期间虚拟机又发现allocation1、allocation2、allocation3 3个2M大小的对象无法放入Survivor空间(Survivor空间大小只有1M),所以只能通过分配担保机制提前转移到老年代。
这次Minor GC结束后,allocation4对象顺利的分配在Eden区中,因此程序执行完内存各区域的分配情况是,Eden区占用4MB(被allocation4占用),Survivor区空闲,老年代占用6MB(被allocation1、allocation2、allocation3占用)。

代码实例1


private static final int _1MB=1024*1024;

/*VM参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
*/
public static void testAllocation(){
    byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1=new byte[2*_1MB];
    allocation2=new byte[2*_1MB];
    allocation3=new byte[2*_1MB];
    allocation4=new byte[2*_1MB]; //出现一次 Minor GC
}

运行结果:


 [GC [DefNew: 6635K->177K(9216K), 0.0075871 secs] 6635K-
 >6321K(19456K), 0.0076708 secs] [Times: user=0.00 sys=0.02, 
 real=0.01 secs]
 Heap
 def new generation   total 9216K, used 2553K [0x26de0000, 
 0x277e0000, 0x277e0000)
 eden space 8192K,  29% used [0x26de0000, 0x27031f50, 
 0x275e0000)
 from space 1024K,  17% used [0x276e0000, 0x2770c728, 
 0x277e0000)
 to   space 1024K,   0% used [0x275e0000, 0x275e0000, 
 0x276e0000)
 tenured generation   total 10240K, used 6144K [0x277e0000, 
 0x281e0000, 0x281e0000)
 the space 10240K,  60% used [0x277e0000, 0x27de0030, 
 0x27de0200, 0x281e0000)
 compacting perm gen  total 12288K, used 386K [0x281e0000, 
 0x28de0000, 0x2c1e0000)
 the space 12288K,   3% used [0x281e0000, 0x282409e8, 
 0x28240a00, 0x28de0000)
 ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
 0x2c749200, 0x2c9e0000)
 rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
 0x2d05d400, 0x2d5e0000)

2、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
通过设置-XX:PretenureSizeThreshold参数,令大于这个阀值的对象直接在老年代分配。这样做的目的是避免在Eden区以及两个Surivivor区之间发生大量的内存复制。
代码实例2中testPretenureSizeThreashold()方法执行后,Eden区几乎没有被使用,而老年代的10M空间被占去40%(4M的allocation对象直接分配在老年代中)。这是因为-XX:PretenureSizeThreshold设置为3M(就是3145728,此参数不能像-Xmx之类的参数那样直接写3M),因此超过3M的对象都会直接在老年代分配。

代码实例2


private static final int _1MB=1024*1024;

/*VM参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreashold() {
    byte[] allocation;
    allocation=new byte[4*_1MB];
}

运行结果:


 Heap
 def new generation   total 9216K, used 819K [0x26de0000, 
 0x277e0000, 0x277e0000)
 eden space 8192K,  10% used [0x26de0000, 0x26eace40,      
 0x275e0000)
 from space 1024K,   0% used [0x275e0000, 0x275e0000, 
 0x276e0000)
 to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
 0x277e0000)
 tenured generation   total 10240K, used 4096K [0x277e0000, 
 0x281e0000, 0x281e0000)
 the space 10240K,  40% used [0x277e0000, 0x27be0010, 
 0x27be0200, 0x281e0000)
 compacting perm gen  total 12288K, used 385K [0x281e0000, 
 0x28de0000, 0x2c1e0000)
 the space 12288K,   3% used [0x281e0000, 0x28240608, 
 0x28240800, 0x28de0000)
 ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
 0x2c749200, 0x2c9e0000)
 rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
 0x2d05d400, 0x2d5e0000)

3、长期存活的对象进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden区,并经过第一次Minor GC后仍然存活且能被Survivor容纳的话,将被移到Survivor区,对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1,当年龄增加到一定程度(默认为15),就将会晋升到老年代中。通过-XX:MaxTenuringThreshold参数设置对象晋升老年代的年龄阀值。
代码实例3,-XX:MaxTenuringThreshold设置为1来执行testMaxTenuringThreashold()方法。方法中的allocation1对象需要256K内存,Survivor空间可以容纳,在第一次Minor GC后,放入Survivor空间。在第二次Minor GC后allocation1进入老年代,新生代已使用的内存变成0K。

代码实例3


private static final int _1MB=1024*1024;

/*VM参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
-XX:+PrintTenuringDistribution
*/
public static void testMaxTenuringThreashold() {
    byte[] allocation1,allocation2,allocation3;
    allocation1=new byte[_1MB/4];//什么时候进入老年代取决于-XX:MaxTenuringThreshold设置
    allocation2=new byte[4*_1MB];
    allocation3=new byte[4*_1MB];//出现第一次Minor GC
    allocation3=null;
    allocation3=new byte[4*_1MB];//出现第二次Minor GC
}

运行结果:


 [GC [DefNew
  Desired survivor size 524288 bytes, new threshold 1 (max 1)
  - age   1:     445288 bytes,     445288 total
  : 5007K->434K(9216K), 0.0071760 secs] 5007K-     
  >4530K(19456K), 0.0072607 secs] [Times: user=0.02 sys=0.00,      
  real=0.01 secs]
 [GC [DefNew
  Desired survivor size 524288 bytes, new threshold 1 (max 1)
  : 4615K->0K(9216K), 0.0014664 secs] 8711K->4530K(19456K), 
  0.0015449 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  Heap
  def new generation   total 9216K, used 4316K [0x26de0000, 
  0x277e0000, 0x277e0000)
  eden space 8192K,  52% used [0x26de0000, 0x272170e8, 
  0x275e0000)
  from space 1024K,   0% used [0x275e0000, 0x275e0000, 
  0x276e0000)
  to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
  0x277e0000)
  tenured generation   total 10240K, used 4530K [0x277e0000, 
  0x281e0000, 0x281e0000)
  the space 10240K,  44% used [0x277e0000, 0x27c4ca60, 
  0x27c4cc00, 0x281e0000)
  compacting perm gen  total 12288K, used 389K [0x281e0000, 
  0x28de0000, 0x2c1e0000)
  the space 12288K,   3% used [0x281e0000, 0x28241668, 
  0x28241800, 0x28de0000)
  ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
  0x2c749200, 0x2c9e0000)
  rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
  0x2d05d400, 0x2d5e0000)

4、对象年龄动态判定

虚拟机并不是永远地等到对象的年龄达到了-XX:MaxTenuringThreshold才能晋升老年代。如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
代码实例4中,设置-XX:MaxTenuringThreshold=15,执行testMaxTenuringThreashold()方法,会发现结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,说明allocation1、allocation2对象进入了老年代,而没有等到15的临界年龄。因为这两个对象加起来已经超过了512K,并且是同年龄的,满足同年龄对象达到Survivor空间的一半,进入老年代的规则。

代码实例4


private static final int _1MB=1024*1024;

/*VM参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution
*/
public static void testMaxTenuringThreashold() {
    byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1=new byte[_1MB/4];
    allocation2=new byte[_1MB/4];//allocation1+allocation2大于Survivor空间一半
    allocation3=new byte[4*_1MB];
    allocation4=new byte[4*_1MB];//出现第一次Minor GC
    allocation4=null;
    allocation4=new byte[4*_1MB];//出现第二次Minor GC
}

运行结果:


  [GC [DefNew
  Desired survivor size 524288 bytes, new threshold 1 (max 15)
  - age   1:     706424 bytes,     706424 total
  : 5099K->689K(9216K), 0.0063450 secs] 5099K-               
  >4785K(19456K), 0.0064277 secs] [Times: user=0.02 sys=0.00,      
  real=0.01 secs]
  [GC [DefNew
  Desired survivor size 524288 bytes, new threshold 15 (max 15)
  - age   1:        264 bytes,        264 total
  : 4949K->0K(9216K), 0.0020751 secs] 9045K->4786K(19456K), 
  0.0021526 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  Heap
  def new generation   total 9216K, used 4345K [0x26de0000, 
  0x277e0000, 0x277e0000)
  eden space 8192K,  53% used [0x26de0000, 0x2721e438, 
  0x275e0000)
  from space 1024K,   0% used [0x275e0000, 0x275e0108,         
  0x276e0000)
  to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
  0x277e0000)
  tenured generation   total 10240K, used 4785K [0x277e0000, 
  0x281e0000, 0x281e0000)
  the space 10240K,  46% used [0x277e0000, 0x27c8c6f8, 
  0x27c8c800, 0x281e0000)
  compacting perm gen  total 12288K, used 389K [0x281e0000, 
  0x28de0000, 0x2c1e0000)
  the space 12288K,   3% used [0x281e0000, 0x28241698, 
  0x28241800, 0x28de0000)
  ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
  0x2c749200, 0x2c9e0000)
  rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
  0x2d05d400, 0x2d5e0000)

5、空间分配担保

在每次Minor GC之前,虚拟机会先检查老年大最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许担保失败,那这是要改为进行一次Full GC。
代码实例5,是在JDK 6 Update 24 之前的版本中运行测试的。之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升到老年代对象的平均大小就进行Minor GC,否则将进行Full GC。

代码实例5


private static final int _1MB=1024*1024;

/*VM参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:+HandlePromotionFailure或者-XX:-HandlePromotionFailure
*/
public static void testHandlePromotion(){
    byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
    allocation1=new byte[2*_1MB];
    allocation2=new byte[2*_1MB];
    allocation3=new byte[2*_1MB];
    allocation1=null;
    allocation4=new byte[2*_1MB];//出现一次Minor GC
    allocation5=new byte[2*_1MB];
    allocation6=new byte[2*_1MB];
    allocation4=null;
    allocation5=null;
    allocation6=null;
    allocation7=new byte[2*_1MB];//设置-XX:+HandlePromotionFailure出现第二次Minor GC 设置-XX:-HandlePromotionFailure 出现Full GC
}

运行结果:


设置-XX:+HandlePromotionFailure

 [GC [DefNew: 6635K->177K(9216K), 0.0059513 secs] 6635K-          
 >4273K(19456K), 0.0060350 secs] [Times: user=0.00 sys=0.00, 
 real=0.01 secs]
 [GC [DefNew: 6573K->178K(9216K), 0.0014895 secs] 10669K-
 >4274K(19456K), 0.0015706 secs] [Times: user=0.00 sys=0.00, 
 real=0.00 secs]

设置-XX:-HandlePromotionFailure

 [GC [DefNew: 6635K->177K(9216K), 0.0056952 secs] 6635K-
 >4273K(19456K), 0.0057779 secs] [Times: user=0.00 sys=0.00, 
 real=0.01 secs]
  [GC [DefNew: 6573K->6573K(9216K), 0.0000482 secs]
  [Tenured: 4096K->4274K(10240K), 0.0125304 secs] 10669K-
  >4274K(19456K), [Perm : 385K->385K(12288K)], 0.0127614 
  secs] [Times: user=0.00 sys=0.02, real=0.01 secs]

参考
[《深入理解Java虚拟机》]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • java语言作为高级语言,最主依赖之一就算是内存的自动分配和回收的功能了。不像c语言那样,需要程序员自己去主动的,...
    联想桥南阅读 273评论 0 2
  • 前言 对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象...
    姜小码阅读 692评论 0 3
  • 对象的内存分配往大的方向讲,是在堆上分配(但也有可能是在栈上分配),对象主要分配在Eden区上,如果启动了本地线程...
    eliter0609阅读 146评论 0 0
  • 声明:此篇文章是读《深入理解JAVA虚拟机》的笔记 对象优先在Eden分配  大多数情况下,对象在新生代Eden区...
    cooolboy阅读 113评论 0 0
  • 天气渐渐热了起来。 因为又增添了新人,中队决定对监舍进行一次大调整。刚好住在金水下铺的那个人出监了,金水算了一下全...
    甘醇阅读 334评论 0 0