Java中的【自动内存管理】可以归结为【自动化】的解决2个问题:
1、给对象分配内存
2、回收分配给对象的内存
对象的内存分配,往大方向说,就是在【堆】上分配:
1、对象主要分配在【新生代】的【Eden区域】上
2、如果启动了【本地线程分配缓冲TLAB】,将按【线程优先】在【TLAB】上分配
一、对象优先在Eden区分配
1、大多数情况下,对象在新生代Eden区中分配。
当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
2、虚拟机提供了:-XX:+PrintGCDetails告诉虚拟机在发生垃圾回收时打印日志并在进程退出的时候输出当前的内存各区域分配情况。
在实际应用中,【内存回收】日志一般是打印到【文件】后通过【日志工具】进行分析
3、Minor GC与Full GC的区别:
【新生代GC(Minor GC)】: 发生在新生代的垃圾收集动作,因为大部分对象具备朝生夕灭特性,所以Minor GC非常频繁,回收速度也比较快
【老年代GC(Major GC/Full GC)】:发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随着至少一次的Minor GC,Major GC的速度会比 Minor GC慢10倍以上。
实验:对象优先在Eden区分配
//测试对象优先在[新生代]的[Eden区]分配
public class ObjectEdenAllocate {
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
}
public static void main(String[] args) {
ObjectEdenAllocate.testAllocation();
}
}
二、大对象直接进入老年代
什么是大对象?
所谓的大对象是指:需要大量【连续内存空间】的【Java对象】
最典型的大对象就是:很长的字符串,数组
大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集获取足够的连续空间来安置它们。
虚拟机提供一个【-XX:PretenureSizeThreshold】参数:
令大于这个设置值的对象直接在老年代分配
三、长期存活的对象进入老年代
虚拟机采用分代收集思想来管理内存,那么内存回收时就必须能识别哪些对象放在新生代哪些对象放在老年代中。
虚拟机给每个对象定义了一个【对象年龄计数器】
如果对象在Eden出生并经过第一次Minor GC后任然存活并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄加1.
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认为15),就会被晋升到老年代中。
对象晋升【老年代】的【年龄阈值】,可以通过参数:
-XX:MaxTenuringThreshold 设置
实验:长期存活的对象将进入老年代
//长期存活的对象将进入[老年代]
public class LongSurvivorObjectEnterOld {
private static final int _1MB = 1024 * 1024;
//VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
public static void testTenuringThreshold() {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1MB / 4];
//什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
public static void main(String[] args) {
LongSurvivorObjectEnterOld.testTenuringThreshold();
}
}
四、动态对象年龄判断
为了更好地适应不同程度的内存状况,虚拟机并不是永远要求【对象年龄】必须达到MaxTenuringThreshold才能晋升老年代
如果在【Survivor空间】中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
五、空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用连续内存空间是否大于新生代所有对象总空间
如果这个条件成立,那么Minor GC可以确保是安全的。
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许【担保失败】。
如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的
如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC
"冒险"是冒了什么风险?
新生代使用复制收集算法,但为了内存利用率,只使用其中【一个Survivor空间】来作为轮换备份,因此当出现大量对象在Minor GC后任然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要【老年代】进行【分配担保】,把Survivor无法容纳的对象直接进入【老年代】
与生活中的贷款担保类似,老年代要进行这样的担保,前提是【老年代】本身还有容纳这些对象的【剩余空间】
一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间
取【平均值】进行比较其实任然是一种动态概念的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致【担保失败(Handle Promotion Failure)】
如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。
虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁