导读
到目前为止我们讲完了:
- 【JVM】自动内存管理机制《一》---内存划分及异常可能情况
- 【JVM】自动内存管理机制《二》---- 内存区域为何划分,以什么原则划分,为何自动管理?
- 【JVM】自动内存管理机制《三》---对象的生死判定和算法详解
- 【JVM】自动内存管理机制《四》---垃圾收集器(索命黑白无常回收垃圾对象)
- 【JVM】自动内存管理机制《五》---垃圾收集器(索命黑白无常回收垃圾对象)
今天我们讲《自动内存管理》部分的最后一篇:【JVM】自动内存管理机制《六》---内存分配与回收策略。为什么讲这个呢?java技术体系所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
关于回收内存这一点我们已经使用了大量篇幅介绍虚拟机中的垃圾收集器体系及运作原理。所以我们接下来再一起探讨下给对象分配内存那点事儿。
内存回收机制
* 对象存活判定算法
* 哪些内存需要回收
* 垃圾收集算法
* 垃圾收集器(对垃圾收集算法的实现)
内存分配与回收策略
* 原则
内存分配策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判定
- 空间分配担保
对象优先在 Eden 分配
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[4 * _1MB]; // 出现一次Minor GC
}
如代码所示,执行 testAllocatio()方法分配了allocation4对象时会发生一次 Minor GC。因为给 allocation4分配内存的时候,发现 Eden 已被占用了6MB,剩余空间已经不够足以分配给 allocation4所需的内存,因此发生了 Minor GC。GC 期间虚拟机又发现已有的3个2MB大小的对象全部无法存放到 Survivor 空间,所以只好通过分配担保机智提前转移到老年代去。
这次GC结束后,4MB的alloction4对象顺利分配在Eden中,因此程序执行完毕结果是Eden占用4MB,Survivor空闲,老年代被占用6MB。
大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的Java对象,典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是个坏消息,经常出现大对象容易导致内存还有不少空间就提前收集以获取足够的连续空间来安置他们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大对象这个值对象直接在老年代分配。这样做目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制。
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M
-XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代中
}
执行代码的testPretenureSizeThreshold() 方法后,allocation对象就直接被分配到老年代中。PretenureSizeThreshold这个参数只对Serial喝ParNew两款收集器有效。
长期存活的对象将进入老年代
既然虚拟机采用了分代手机的思想来管理内存,那么内存回收旧必须能识别哪些对象应该放到新生代,哪些对象应该放到老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设定为1。对象在Survivor区中熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代。可以通过-XX:MaxTenuringThreshold设置。
动态对象年龄判定
为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M
-XX:+PrintGCDetails -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4]; // allocation1+alloca
tion2大于survivo空间一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
执行代码会发现Survivor的空间仍然为0%,而老年代比预期增加了6%,也就是allocation1,allocation2对象直接进入了老年代,而没有等到15岁的年龄。因为两个对象加起来已经到达了512KB,并且他们是同年的。
空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。,如果大于,将尝试一次Minor GC,尽管这次GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那么这时要改为进行一次Full GC。
所谓的冒险,是新生代采用的复制收集算法,但为了内存利用率,只使用一个Survivor空间作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况,就是需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,提前保证老年代本身还有容纳这些对象的剩余空间,一共会有多少对象会存活下来在实际完成内存回收前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量大平均大小值作为经验值,与老年代的剩余空间比较,决定是否进行Full GC来让老年代腾出更多的空间。
多次提到Minor GC 喝 Full GC,两者之间有什么不一样吗?
Minor GC是新生代GC,发生在新生代的垃圾回收动作,因为Java对象大多都是具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也很快。
Full GC是老年代的GC,发生在老年代的垃圾回收动作,会经常伴随至少一次的Minor GC。Full GC执行速度一般比Minor GC慢十倍以上。
本次的内存分配与回收策略分享结束,谢谢阅读。?️?️?️