JVM垃圾回收详解 (2)

标记整理算法解决了标记清除算法产生内存碎片的问题,同时也解决了复制算法只能利用一半内存的问题,看似是非常的完美。但是,它却产生了另外一个问题。可以看到图中,内存的变动非常频繁,每次整理都有很多存活的对象内存地址发生改变。因此,它的效率会慢很多。

所以,现在一般用分代收集算法。在Java堆中,分为新生代和老年代,可以根据各个代的特点,选择最合适的收集算法。新生代中,每次垃圾收集都有大批对象死去,只有少量对象存活,就可以选择复制算法,只需要付出少量存活对象的复制成本即可。而老年代中,对象存活率高,没有额外空间对它进行分配担保,因此使用标记清除或者标记整理算法。

堆内存模型

Java堆是内存管理中最大的一块区域,也是垃圾回收的重点区域。堆分为新生代、老年代和永久代,新生代又分为Eden区和Survivor区,Survivor又分为S0和S1区。在JDK1.8之后把永久代移除了,而用元空间代替。(永久代使用的是堆内存,而元空间直接使用本机物理内存)

file

新生代中的对象98%都是朝生夕死的,因此把新生代分为较大的一块Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor(此处Survivor区也叫From区,另一块空的未使用的空间叫To区,From和To区是会交换的,保证空的总是To区)。

当Eden区没有足够的空间分配时,会进行一次Minor GC,Eden区大部分对象都被回收,而Eden区和From区存活的对象会放入到To区,然后From和To区进行交换。(如果To区空间不够,直接进入老年代)

以下几种情况会进入老年代。

1) 大对象

大对象就是指需要大量连续内存空间的对象,最典型的就是那种很长的字符串和数组。大对象会直接进入到老年代,这样做的目的主要是为了避免新生代发生大量的内存复制(大对象的复制成本较高)。

2)长期存活的对象

虚拟机给每个对象都定义了一个对象年龄计数器。每当进行一次Minor GC,年龄就增加1岁,当年龄超过一定值时(默认是15,可以通过参数配置),就进入到老年代。

3)动态对象年龄判断

虚拟机并不要求对象年龄一定要到达15岁才进入到老年代。如果Survivor空间中有某年龄相同的所有对象大小总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就会直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那么Minor GC可以确保是安全的(因为,极端情况下,就算新生代所有对象都存活,也可以保证安全晋升到老年代)。否则,虚拟机会查看HandlePromotionFailure的值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试进行一次Minor GC(尽管有风险);如果小于或者HandlePromotionFailure设置不允许冒险,那么就先进行一次Full GC。

以上说的有风险,是因为取历次晋升到老年代对象的平均值这种方式只是经验值,并不能保证每次都能担保成功,如果担保成功还好,如果担保失败的话,依然需要进行Full GC。

尽管如此,我们最好还是打开HandlePromotionFailure开关,避免过多频繁的Full GC(因为Full GC的执行速度比Minor GC慢的多)。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpfpgg.html