首页 JVM对象回收机制(下)
文章
取消

JVM对象回收机制(下)

我们知道当内存空间达到一定条件时,会自动触发。这个过程就叫作 GC,负责 GC 的组件,就叫作垃圾回收器。按照语义上面理解,垃圾回收首先需要找到的是这些垃圾,然后回收掉,但是GC过程正好相反,它是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除。所以垃圾回收只与活跃的对象有关,和堆的大小无关。

标记-清除算法

就像我们前面说的,GC过程正好相反:垃圾回收的第一步,就是找出活跃的对象,并将它们标记起来。根据GC Roots从上到下遍历所有可达对象,这个过程就叫做标记。image-20220822173744871

如上图所示,绿色代表GC Roots,蓝色代表可以被引用到的对象,灰色代表被回收的对象。但是这种方式有一个弊端就是会产生大量不连续的碎片,空间碎片太对可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次GC动作。

复制算法

将堆分为两个大小相同的空间 From 和 To, 利用 From 空间进行分配,当 From 空间满的时候,GC将其中的活动对象复制到 To 空间,之后将两个空间互换即完成GC。虽然解决了碎片问题,但是弊端也很明显,就是将内存缩小为了原来的一半,如果资源本身有限,这就是非常高的代价了。

标记-整理算法

image-20220822175052116

它的主要思路就是,标记过程和标记-清除算法一样,但是到了整理阶段,它会移动所有存活的对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部回收掉。这种算法虽然效率差一些,但是没有空间浪费,也没有产生碎片。

分代收集算法

综上,没有最优的算法,只有最合适的算法。分代收集没有新的思想,只是根据对象的存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集时有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中因为对象存活率高,没有额外空间对他们进行分配担保,就必须使用标记-清理或者标记-整理算法进行回收。

新生代

综合上面,年轻代使用的垃圾回收算法是复制算法。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。我们前面也了解到复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域:一个伊甸园空间(Eden ),两个幸存者空间(Survivor )

当新生代中的 Eden 区分配满的时候,就会触发新生代的 GC(Minor GC)。具体过程如下:

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称from);
  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。

所以在这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。

这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8)。

老年代

结合上面,老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。

那么新生代对象如何进入老年代的呢?它们有这么几种:

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

    超出某个大小的对象将直接在老年代分配。这个值是通过参数 -XX:PretenureSizeThreshold 进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。

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

    新生代对象每当发生一次 Minor GC,存活下来的对象年龄都会加 1。直到达到一定的阈值,就会把这些“老顽固”给提升到老年代。这个阈值,可以通过参数 ‐XX:+MaxTenuringThreshold 进行配置,最大值是 15(默认)。

    这些对象如果变的不可达,直到老年代发生 GC 的时候,才会被清理掉。

  3. 动态对象年龄判断

    为了更好地适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才能提升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

  4. 空间分配担保

    上面新生代每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。

本文由作者按照 CC BY 4.0 进行授权

JVM对象回收机制(上)

Java内存模型JMM与多线程