0%

内存分配

内存分配

Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以 及自动回收分配给对象的内存。关于回收内存这方面,在前文的垃圾回收相关文章中已经介绍。本篇文章探讨一下关于给对象分配内存的那些事儿。

这里主要介绍JVM通用的分配规则,不通的垃圾回收器可能会有写差别,但是大体都是相似的。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区进行分配。当Eden区没有足够空间进行分配时JVM发生一次Minor GC。因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,当然了,其回收速度肯定也是比较快的,与之对应,还有个Full GC或者称为Major GC,是指老年代中的GC,经常会伴随一次Minor GC,Major GC速度一般会比Minor GC速度慢10倍以上!

大对象直接进入老年代

所谓的大对象,是指占用大量连续内存空间的Java对象。最经典的大对象就是那种很长的字符串和数组。大对象对于虚拟机来说是个坏消息,我们写程序时,尽量要避免出现一群朝生夕死的大对象。经常出现大对象容易导致内存还有不少空间时就得提前触发垃圾收集以获取足够的空间来存放大对象。

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

VM采用分代收集思想来管理内存,就要去区分哪些是年轻代的对象,哪些是老年代的对象。我们知道,刚创建的对象肯定是年轻的对象,那么怎么将对象判断为老年代?

在Eden区出生,并经过一次Minor GC后仍然存活,并且能被To Suvivor容纳,移动到To Suvivor区后,年龄设置为1。以后每经历一次Minor GC就将年龄加1,当它的年龄达到一个阀值(默认15,也可以更改-XX:MaxTenurinigThreshold来设置),就会被晋级到老年代中。

动态对象年龄判定

为了更好地适应不同程序内存情况,JVM并不一定是等到对象年龄达到阀值才将对象晋级到老年代。如果在Survivor空间中的相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到指定的阀值。这句话可能有点绕,不太好理解,我来再解释一下,就是说,假设Survivor的空间大小为max,年龄为y的对象总共有n个,如果y*n>max/2,那么所有年龄大于y的对象全部进入到老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代可用的连续空间是否大于所有新生代的总空间,如果大于的话,那么这个GC就可以保证安全,如果不成立的,那么可能会造成晋升老年代的时候内存不足。在这样的情况下,虚拟机会先检查HandlePromotionFailure设置值是否允许担保失败,如果是允许的,那么说明虚拟机允许这样的风险存在并坚持运行,然后检查老年代的最大连续可用空间是否大于历次晋升老年代对象的平均大小,如果大于的话,就执行Minor GC,如果小于或者HandlePromotionFailure设置不允许冒险,那么就会先进行一次Full GC将老年代的内存清理出来,然后再判断。

上面提到的风险,是由于新生代因为存活对象采用复制算法,但为了内存利用率,只使用其中的一个Survivor空间,将存活的对象备份到Survivor空间上,一旦出现大量对象在一次Minor GC以后依然存活(最坏的计划就是没有发现有对象死亡需要清理),那么就需要老年代来分担一部分内存,把在Survivor上分配不下的对象直接进入老年代,因为我们不知道实际上具体需要多大内存,我们只能估算一个合理值,这个值采用的方法就是计算出每次晋升老年代的平均内存大小作为参考,如果需要的话,那就提前进行一次Full GC.

取平均值在大多数情况下是可行的,但是因为内存分配的不确定性太多,保不定哪次运行突然出现某些大对象或者Minor GC以后多数对象依然存活,导致内存远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。这样的情况下,担保失败是要付出代价的,大部分情况下都还是会将HandlePromotionFailure开关打开,毕竟失败的几率比较小,这样的担保可以避免Full GC过于频繁,垃圾收集器频繁的启动肯定是不好的。在JDK6之后这是默认打开的。