内存分配与回收策略

对象的内存分配主要是在新生带的Eden上,如果启动了本地线程分配缓冲,就按线程优先级分配在TLAB上,还会有少数情况直接分配在老年代。内存分配的规则不是固定了,细节还是取决于垃圾收集器组合。

下面是一些普遍的内存分配规则:

  1. 对象优先在Eden分配
  2. 大对象直接进入老年代(PretenureSizeThreshold)
  3. 长期存活的对象将进入老年代(MaxTenuringThreshold)
  4. 动态对象年龄判断(相等年龄对象占Survivor空间一半)
  5. 空间分配担保流程

但是这些规则并不是固定的,还是会根据具体情况而定,程序测试会将JVM参数卸载方法前的注释中。

一、对象优先在Eden分配

一般情况下对象优先在Eden上分配,如果Eden空间不足,就进行一次MinorGC。

Minor GC指发生在新生代的垃圾回收,Minor GC比较频繁,一般回收速度也比较快。Major GC/Full GC指发生在老年代的GC,经常会伴随至少一次的Minor GC(非绝对),Major GC的速度一般比Minor GC慢十倍以上。

设置注释中的参数,此时新生代老年代分别有10m的空间,其中新生代的Eden和Survivor比为8:1,所以此时Eden只有8m。按照上面的观点,在分配a4时,Eden剩余空间已经不足容下a4,所以会进行一次Minor GC。

在我的测试环境下,程序中什么都不写时,堆的Eden中也会有2.5546875m的空间被使用,并且这2点多m在GC时能回收掉一部分。

public class EdenTest {
    // -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
    // -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] a1, a2, a3, a4;
        a1 = new byte[_1MB];
        a2 = new byte[_1MB];
        a3 = new byte[_1MB];
        a4 = new byte[5 * _1MB];
    }
}

运行程序,查看GC日志。

// 发生了Minor GC,新生代使用空间大小从5238k变成了655k
[GC (Allocation Failure) [DefNew: 5238K->655K(9216K), 0.0029147 secs] 5238K->3727K(19456K), 0.0029604 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 5857K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  // eden中存放着a4
  eden space 8192K,  63% used [0x00000000fec00000, 0x00000000ff114930, 0x00000000ff400000)
  from space 1024K,  64% used [0x00000000ff500000, 0x00000000ff5a3e00, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 // 老生代使用的空间正好为3m,也就是a1、a2、a3移入了老年代
 tenured generation   total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  30% used [0x00000000ff600000, 0x00000000ff900030, 0x00000000ff900200, 0x0000000100000000)
 Metaspace       used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

GC日志表现得和推论差不多。程序运行中的内存分配主要是这样一个步骤:

  1. 一些未知的东西在eden中分配占用了2m多。
  2. a1分配进入eden。
  3. a2分配进入eden。
  4. a3分配进入eden。
  5. eden剩余空间不足安置a4,发起一次Minor GC。
  6. 未知的东西一部分被回收,一部分被移入Survivor(小于1m的部分)。
  7. a1存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。
  8. a2存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。
  9. a3存在引用,不会被回收,但是Survivor装不下,通过分配担保机制直接进入老年代。

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

大对象就是分配一段连续大大空间。大对象的经常出现容易导致内存中还有不少空间时提前出发GC,来保证能够获取足够的连续空间,写程序时应该避免。

Serial和ParNew提供了PretenureSizeThreshold参数,当分配大于等于这个参数值的对象时,就会直接将对象分配在老年代中。

设置PretenureSizeThreshold为3m,运行下面程序,a1应该被直接分配在老年代,哪怕此时新生代的空间足够存放a1。

public class ThresholdTest {
    // -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
    // -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
    // -XX:PretenureSizeThreshold=3145728
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] a1 = new byte[3 * _1MB];
    }
}

看输出的GC日志,确实a1被分配在了老年区。

Heap
 def new generation   total 9216K, used 2330K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  28% used [0x00000000fec00000, 0x00000000fee469f8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   // 老年代有30%的空间被使用,正好是3m,也就是a1直接分配在这里。
   the space 10240K,  30% used [0x00000000ff600000, 0x00000000ff900010, 0x00000000ff900200, 0x0000000100000000)
 Metaspace       used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

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

分代管理其实是虚拟机给每个对象都定义了一个对象年龄计数器,每经过一次Minor GC且没有被回收,这个计数器就会加一,当到达一定大小时,这个对象就会被移到老年代中,阈值通过-XX:MaxTenuringThreshold设置。

还是因为那未知的2m多,我调整了一下书上的测试程序。

设置MaxTenuringThreshold为1,所以下面经历了两次GC后,新生代区的所有内容都应该进入老年代。

public class MaxTenuringTest {
    // -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
    // -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8
    // -XX:MaxTenuringThreshold=1
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] a1, a2, a3;
        a1 = new byte[_1MB / 512];
        a2 = new byte[8 * _1MB];
        a3 = new byte[8 * _1MB];
        a3 = null;
        a3 = new byte[8 * _1MB];
    }
}

确实,在第二次GC后,新生代全部空了,因为已经达到阈值,晋升到老年代了。

[GC (Allocation Failure) [DefNew: 10749K->911K(18432K), 0.0052187 secs] 10749K->9103K(38912K), 0.0052600 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
// 新生代清空
[GC (Allocation Failure) [DefNew: 9103K->0K(18432K), 0.0011062 secs] 17295K->9099K(38912K), 0.0011249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 18432K, used 8356K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
  from space 2048K,   0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 9099K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  44% used [0x00000000fec00000, 0x00000000ff4e2e80, 0x00000000ff4e3000, 0x0000000100000000)
 Metaspace       used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

调整参数为10后日志如下,新生代仍有内容。

[GC (Allocation Failure) [DefNew: 10749K->911K(18432K), 0.0047404 secs] 10749K->9103K(38912K), 0.0048049 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// 新生代内容保持着
[GC (Allocation Failure) [DefNew: 9103K->907K(18432K), 0.0010164 secs] 17295K->9099K(38912K), 0.0010356 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
Heap
 def new generation   total 18432K, used 9591K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  53% used [0x00000000fd800000, 0x00000000fe07afb0, 0x00000000fe800000)
  from space 2048K,  44% used [0x00000000fe800000, 0x00000000fe8e2e70, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 8192K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  40% used [0x00000000fec00000, 0x00000000ff400010, 0x00000000ff400200, 0x0000000100000000)
 Metaspace       used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

四、动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不是永远需要对象年龄到达MaxTenuringThreshold大小时才能进入老年代。

如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半时,年龄大于等于该值的所有对象可以进入老年代。

下面程序中a1加未知的空间已经大于一个Survivor的一半空间了,所以在第二次GC时会判定这两部分进入老年代,哪怕MaxTenuringThreshold设置的是10。

public class DynamicTest {
    // -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
    // -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8
    // -XX:MaxTenuringThreshold=10
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        byte[] a1, a2, a3;
        a1 = new byte[_1MB];
        a2 = new byte[8 * _1MB];
        a3 = new byte[8 * _1MB];
        a3 = null;
        a3 = new byte[8 * _1MB];
    }
}

第二次GC后新生代为空,全部进入老年代了。

[GC (Allocation Failure) [DefNew: 11517K->1679K(18432K), 0.0052658 secs] 11517K->9871K(38912K), 0.0053102 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] 
// GC后新生代为空
[GC (Allocation Failure) [DefNew: 9871K->0K(18432K), 0.0014142 secs] 18063K->9867K(38912K), 0.0014360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 18432K, used 8356K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
  from space 2048K,   0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 9867K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  48% used [0x00000000fec00000, 0x00000000ff5a2e80, 0x00000000ff5a3000, 0x0000000100000000)
 Metaspace       used 3494K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

五、空间分配担保

在JDK1.6之后,发生Minor GC之前,虚拟机会检查老年代连续空间是否大于新生代对象总大小或历次晋升平均大小,如果连续空间大,则直接Minor GC,否则会先进行一次Full GC,来提高分配担保的成功率。

取晋升平均值其实是一种动态概率的手段,但是无法完全避免担保失败的,如果出现了某次Minor GC存活对象远大于均值,就失败了,失败后会重新发起一次Full GC。

在JDK1.6之前,有一个HandlePromotionFailure(是否允许分配担保失败)的参数,在判断完老年代连续空间小于新生代对象总大小后,会判断是否设置此参数。如果允许分配担保失败才会去检查老年代连续空间是否大于平均晋升大小。在JDK1.6之后,修改了HotSpot中空间分配检查的代码片段,这个参数不再起作用。


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

推荐阅读更多精彩内容