深入理解Java虚拟机第三章读书笔记:内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:

  1. 给对象分配内存
  2. 回收分配给对象的内存

对象的内存分配,从大体上讲,就是在堆内存中进行分配,对象主要是分配在新生代的Eden区上,如果启动了本地线程分配缓冲(TLAB),则将按线程优先在TLAB上分配。少数情况也会直接分配在老年代中,分配的规则不是百分之百固定的,细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

一下是几条最普遍的内存分配规则。


MinorGC:在eden区进行内存回收

MajorGC:在old区进行内存回收

FullGC:新生代和老年代都进行内存收回

更新

有一些对象其实可以在栈上面进行分配,当它们的作用域是某个方法体内部的时候就可以这么干,这些对象随着方法调用结束而自动销毁,不需要垃圾处理器来对它进行收集处理,可以提高系统的性能。

但是在栈上分配对象有两个技术基础:

  1. 要先进行逃逸分析,判断这个对象是否有可能逃出这个方法体,但是只有在Server模式下才能开启逃逸分析,目前Java版本中的HotSpot虚拟机默认开启逃逸分析。但是逃逸分析会付出时间成本,其实对性能的提升特不是特别显著,而且现在为了压缩逃逸分析的时间成本,采用的都是预测不那么准确且时间成本低的算法,从而导致这项技术不是特别稳定,要慎用
  2. 标量替换,允许将对象打散分配在栈上,假如这个对象有两个字段,会将这两个字段作为局部变量在栈上分配

1.对象优先在Eden分配

大多数情况下,对象在新生代的Eden区中分配。当Eden区中没有足够的内存空间进行分配时,虚拟机将发起一次MinorGC(新生代GC,分配率越高,MinorGC越频繁,但是回收速度一般也比较快)。

测试代码:

private static final int_1MB = 1024 * 1024;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -xx:+PrintGCDetails -xx:SurvivorRatio=8
*/

public static void testAllocataion(){
    byte[] allocation,allocation2,allocation3,allocation4;
    allocation 1 = new byte[2 * _1MB];
    allocation 2 = new byte[2 * _1MB];
    allocation 3 = new byte[2 * _1MB];
    allocation 4 = new byte[4 * _1MB];        //出现一处MinorGC
}

实验参数设置:

-Xms20M   -Xmx20M    -Xmn10M这三个参数限制了Java堆的大小为20M且不可扩展,其中10M分给新生代,10M分配给来年代

-xx:SurvivorRatio=8 确定了新生代中Eden区与Survivor区的空间比例是8:1(新生代中有两个Survivor区)

实验结果:

testAllocation()方法执行,在分配allocation4对象时会触发一次MinorGC,原因是在给allocation4分配对象时候,发现Eden区域已经被占用了6MB,剩余空间无法容纳allocation4对象的4MB内存。GC期间虚拟机又发现已经分配的3个大小为2MB的对象全部无法放入Survivor空间中(因为Survivor空间只有1MB),所以只能通过分配担保机制将这三个对象提前转移到老年代中,而MinorGC后allocation4对象将分配到Eden区域。

GC结束后内存分配情况:Eden占用4MB(allocation4),Survivor空闲,老年代占用6MB(allocation1、2、3)

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

所谓的大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(上面代码中的byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说是一个坏消息(特别是“朝生夕灭”的“短命大对象”,写程序时应该避免),经常出现大对象容易导致内存还有不少空间时就要提前触发垃圾收集以获取足够的连续空间来安置它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,目的是当分配大于这个值的对象时,直接在老年代中分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制。

Q:为什么新生代中要设置两个Survivor区?

A:因为要保证任何时候都有一个Survivor区是空的,避免导致出现内存碎片而浪费了内存空间。触发MinorGC时,会先把存活的对象从Eden区复制到Survivor区,然后再清空Eden区的空间。如果只有一个Survivor区的话,那么触发第一次MinorGC时,存活对象复制到Survivor,Eden清空,触发第二次MinorGC时,存活对象又从Eden转移到Survivor,那么这两次复制过来的对象内存地址肯定是不连续的,就会产生内存碎片,导致空间浪费。如果有两个Survivor区的话,任何时候保持一个Survivor是空的,就不会造成这种情况。(在Survivor区交换14次以后,晋升到老年代

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

既然虚拟机采用了分代手机的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。

如果对象在Eden中出生并经过第一次MinorGC后任然可以存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且设置对象年龄为1岁,对象在Survivor区中每熬过一次MinorGC,年龄就会增加一岁,当他年龄增加到一定程度时(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的阀值可以通过参数-XX:MaxTenuringThreshold设置。 

4、动态对象年龄判定

为了更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代。

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间大小的一半,年龄大于或等于该年龄的对象就会直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。

5、空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,会继续检查老年代最大可用的连续空间是都大于历次晋升到老年代对象的平均大小,如果大于,会尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那么这时也要改为进行一次Full GC。

新生代使用的是复制收集算法,为了保证内存空间的利用率,每次只能使用一块Survivor空间,因此当出现大量对象在MinorGC后依然存活,而Survivor空间不足够存放这些对象时,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。这与生活中的贷款担保相似,老年代要进行担保,那么就要确保老年代的内存空间大小足够去容纳这些对象。

有多少对象会活下来在实际完成前是无法明确得知的,所以只好取之前每一次晋升到老年代中的所有对象大小的平均值,来与老年代的剩余空间进行比较,从而决定是进行Full GC来让老年代腾出更多的空间。

取平均值并不是每一次都会成功,有可能这一次存活下来的对象所占内存空间远超这个平均值,那么依然会导致担保失败,担保失败以后会重新发起一次Full GC。虽然担保失败会绕很大一个圈子,但是为了避免Full GC太频繁,大部分情况下还是会把HandlePromotionFailure开关打开。

猜你喜欢

转载自blog.csdn.net/weixin_41047704/article/details/85006073