一些收藏个人学习

深入JVM内核11 JVM内存分配

2020-02-27  本文已影响0人  香沙小熊

理解JVM内存分配策略

JVM分配内存机制有三大原则和担保机制
具体如下所示:

1. 对象优先在Eden区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。
在测试之前我们先来看看 Minor Gc和Full GC 有什么不同呢?
新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

public class GCTest {
    public static void main(String[] args) {
        byte[] allocation1;
        allocation1 = new byte[60000 * 1024];
    }
}

VM中 添加的参数

 -XX:+PrintGCDetails

运行结果:


image.png

从上图我们可以看出eden区内存几乎已经被分配完全,发现什么都不干新生代额外花费了5000(65024-60000)多k内存。

注意:额外花费的内存与新生代的实际花费的有关,实际花费越大,额外花费的内存的内存也越大。

public class GCTest {
    public static void main(String[] args) {
        byte[] allocation1,allocation2;
        allocation1 = new byte[60000 * 1024];
        allocation2 = new byte[2000 * 1024];
    }
}
image.png
为什么会出现这种情况:

因为给allocation2分配内存的时候eden区内存几乎已经被分配完了,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。GC期间虚拟机又发现allocation1无法存入Survior空间,所以只好通过分配担保机制把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,所以不会出现Full GC。执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。

代码验证:

public class GCTest {
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[60000 * 1024];
        allocation2 = new byte[2000 * 1024];
        allocation3 = new byte[4000 * 1024];
    }
}
image.png

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

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

为什么要这样呢?

为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

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

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

JVM的分代年龄为什么默认是15?

事情是这样的,HotSpot虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark word”。

例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0 。

对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可能为16,20之类的了。

4.新生代的Eden:Survivor from:Survivor to = 8:1:1

垃圾回收中为什么新生代的Eden:Survivor from:Survivor to = 8:1:1

因此,JVM开发人员将新生代分为一块较大的Eden区,和两块较小的Survivor区,每次可以使用来存放对象的是Eden区和其中一块Survivor区。当回收时,将Eden区和Survivor from中还存活着的对象一次性复制到另一块Survivor to区(这里进行复制算法),然后就清空调Eden区和Survivor from区中的数据。

这样新生代中可用的内存:复制算法所需要的担保内存 = 9:1,这样即使所有的对象都不会存活,那么也只会“浪费”10%的内存空间。不过我们也无法保证存活的对象一定<2%或10%,当新生代中Survivor to区内存不够用时,就会触发老年代的担保机制进行分配担保。

之所以Eden区:Survivor from区是8:1,是因为JVM规定,两个Survivor区中from和to是相对的,根据每次进行MinorGC后哪个区被清空没有对象了,这个区就会成为to区,而通过复制算法复制的还存活下的对象所在的那个区,也就是有对象的区即为from(即from和to区会进行位置交换,所以在我们讲解新生代时,还会给这两个Survivor区加上S1和S2两个名称,而S1和S2位置则是固定的)

总结:

4. JVM空间分配担保(失败担保机制)

JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次MinorGC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多);如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次FGC。

上述所说的冒险到底是冒的什么险呢?

前面提到过,新生代使用复制收集算法,但是为了内存利用率。只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况是内存回收之后,新生代中所有的对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象存活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现HandlerPromotionFailure失败,那就只好在失败后重新发起一次FULL GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是将HandlerPromotionFailure开关打开,避免Full GC过于频繁。

特别感谢:

图灵学院

上一篇下一篇

猜你喜欢

热点阅读