JVM学习日记⭐️垃圾收集算法之三种基础算法的梳理⭐️

205 阅读5分钟

“这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

🔉引言

昨天提到了,垃圾收集的三种假说,今天我们继续深入讲解垃圾回收算法的出现过程。

📰 标记-清除

标记-清除(Mark-Sweep)算法是出现的最早也是最基础的算法了,与它的名字一样,整个算法的处理分为两个阶段:标记清除。标记过程属于判断对象是否是垃圾的过程(传送门),清除就是清理垃圾的过程,在标记完成后,统一回收所有未被标记的对象。

为什么称其为基础算法呢?就是因为我们后续的绝大多数收集算法都是基于该算法改进的,谈到改进了,就不得不提它的不足了,大概有这么两个:

  • 执行效率不稳定
  • 容易产生空间碎片

执行效率怎么理解呢?如果java堆中包含大量的“顽固”对象,那我们就不得不进行大量的标记和清除操作,就是对象越多,我们效率越低。那回收之后还会产生大量的空间碎片,就是大量不连续的空间,这可让我们的java虚拟机犯难了,因为在分配大对象的时候找不到足够连续的内存了,那就不得不再again一次,示意图如下所示:

image.png

📰 标记-复制

标记-复制算法也叫复制算法,这个算法怎么玩呢,哎,别急我来教你,我们可以把使用的内存划分为两块,注意大小相等哦,然后我们每次只用一块,用完这块,就将还存活的对象放到另一块上面,然后再把已经使用的空间一次性处理掉。不过这样做,对于多数对象都是存活的情况来看,就会增加额外的复制开销,那对于多数对象都是需要回收的对象,那就很友好了,复制的开销也少,同时还不用考虑空间碎片的复杂情况,因为我现在一次只使用一半空间了,遇到大对象,只需要移动堆顶指针,按顺序分配即可,这样实现简单高效,觉得好的给点个赞吧(忽悠到位),示意图如下所示: image.png

标记复制的缺陷也很明显,就是我们不能使用这整块内存了,总有一半内存空闲。那我们就得解决啊,我们能不能不按1:1的比列划分新生代呢,这个IBM公司发现了一个现象:98%的对象在新生代中熬不过第一轮的垃圾收集,这一现象也叫朝生夕灭

根据这一现象,1989年我们提出了更优化的半区复制分代策略,这种策略也被HotSpot虚拟机的新生代收集器ParNew、Serial所采用,怎么玩的呢?就是我们不要1:1划分新生代了,我们先划分出一块较大的空间-Eden区,再弄两个小的Survivor区,比列按8:1:1分配,我一次就采用一个Eden+一个Survivor就够玩了,那当发生垃圾回收时,我就把存活的对象都复制到另一块Survivor中进行保存,然后直接清理使用过的SurvivorEden,有点像我们生活中把肉吃完了,将骨头放另一个小盘子保存,哈哈用来喂狗。

但是我们剩的骨头把盘子装满了怎么办?肉太好吃了,吃了很多。那我们就需要动用额外的盘子-老年代来进行内存担保了,担保机制先按下不表。

📰 标记-整理

标记-复制算法在存活对象比较多的时候,复制的效率比较低,而且,如果不想浪费一半的内存空间就需要额外的空间做内存担保,这新生代可以找老年代担保,那老年代找谁啊?所以对于老年代来说,不能直接选用这个算法。

那怎么办呢?我们就提出了一种新的算法:标记-整理,怎么玩的呢?就是在标记存活对象的基础上,将所有存活对象移动到内存的另一块区域,然后回收掉边界外的区域就可以了,如下图所示: image.png

那我们知道老年代有很多是“顽固”对象,那每次回收都要移动这么多对象,并更新它们的引用也是一个繁重的操作(ZGC先按下不表),而且还要全程暂停用户程序才能进行,这对使用者不太友好,这种设计被我们称之为Stop the Wrold

那我们知道了是否移动对象都会有弊端,如果你决定,移动对象,那回收的时候就会带来内存回收时的复杂性,你说那我不移动,那就会在分配对象时犯难。从垃圾回收的停顿时间来看,似乎不移动对停顿时间更友好。但从程序的吞吐量来讲,移动似乎更划算一些。吞吐量实际上是赋值器与收集器的效率总和,不移动对象确实会使收集器的效率更高一些,但是我们的内存分配远比垃圾收集的频率要高得多,那这部分耗时增加,总的吞吐量还是下降的。

题外话