美文网首页
JVM垃圾回收中的算法

JVM垃圾回收中的算法

作者: 自天佑之吉无不利 | 来源:发表于2023-09-20 20:59 被阅读0次

垃圾回收算法种类很多,它是不断演进的各种垃圾回收器的理论基础。掌握垃圾回收算法,能帮助我们看清 JVM 中垃圾回收器的本质和演进趋势。

标记 - 清除算法(Mark-Sweep)

标记 - 清除算法是最早提出并实现的垃圾回收算法,虽然和现在越来越智能的垃圾回收算法相比,标记 - 清除算法显得非常简单,但是它却是后面这些垃圾回收算法的思想基础和发展之源。可以认为我们现在使用的各种垃圾回收算法都是基于标记 - 清除算法的思想不断改进、演化而来的,所以标记 - 清除算法是必须要掌握的。

在标记 - 清除算法中,我们把垃圾回收的过程划分成标记和清除两个阶段。

  1. 标记阶段
    基于可达性分析算法,从根对象开始遍历所有的可达对象并标记它们。

  2. 清除阶段
    清除所有未被标记的对象。这个地方需要注意的是清除对象的顺序并不影响算法的结果。最简单的清除算法就是逐一检查每个对象并释放未被标记的对象。

标记 - 清除算法的一个重要挑战在于如何有效地处理空间碎片,因为每次回收后内存中都会存在大量不连续的碎片,即使这些内存碎片的空间总和足够装下这个对象,也没办法有效地分配给大型对象,从而影响 JVM 的性能和可靠性。

标记 - 清除算法除了会引发碎片化问题,还存在执行效率的问题。因为标记对象和清理对象需要消耗一定的时间,如果遇到拥有大量对象的应用场景的话,可能会导致暂停时间过长。

复制算法(Copying)

复制算法在处理大量短生命周期的对象时,比如处理 Web 请求时的临时对象,复制算法简单高效。

和清除算法相比,复制算法效率更高,可以有效地处理大量的内存。在复制算法下,JVM 把可用的堆内存分成两个相等的部分,只在其中一部分(From 区)里分配对象。另一部分(To 区)暂时不使用。所有新创建的对象都会被分配到 From 区。

复制算法分成标记、复制、清理、交换四个阶段。

和标记 - 清除算法相比,复制算法可以有效地把内存空间划分成两部分,每部分都可以分配给不同的对象,这样就可以省去标记和清除的步骤,大大提高了执行效率。并且因为活动对象会被复制到新的内存区域,只要新的内存区域有足够的空间就不会产生碎片。垃圾收集的同时进行对象的移动和内存分配,没有了标记和清除的过程,应用运行不会被暂停太长时间,解决了垃圾收集的停顿问题。

虽然复制算法有很多优点,但是缺点也同样明显。最主要的就是内存利用率低的问题。复制算法需要把内存空间分为相等大小的两部分,但只使用一部分,这导致可用内存空间只有原来的一半。虽然可以省去标记和清理的步骤,但它还是会消耗大量的时间在对象的拷贝上,特别是在对象的生命周期比较长的时候,更加繁琐,会导致整个系统的运转速度变慢。所以在实际的生产实践中,一般是以复制算法为基础,使用它的衍生版本。

复制算法改进版(Copying Advanced)

在这个改进版中,它把内存空间分成一个比较大的 Eden 区和两个小一点的 Survivor 区,这个模型从 JDK 2.0 版本后开始使用。就像搬家一样,我们会把 Eden 区的东西分好类,该回收的回收,还存活的对象会搬到 Survivor To 区域,然后再把原来的 Survivor From 子区中还存活的对象也搬到 Survivor To 区域,Survivor From 子区清空。每次垃圾回收周期结束后, Survivor To 区就会成为 Survivor From 区,两者角色互换,开始新一轮的垃圾回收。

image.png

如果 Survivor 空间不够用了,我们就需要老年代(更大的内存资源)来帮忙。

总的来说,复制算法改进版解决了一般复制算法在内存利用上的问题,避免了将内存“一刀两断”的情况。分配对象内存更加简单和快捷,只需简单的指针判断和位移即可。但是,这个算法的垃圾回收过程需要暂停用户线程,所以如果 Survivor 空间突然间不够用了,就可能会导致提前进行 Full GC,这也就意味着扩大了回收范围,影响性能。这种模型适用于对象存活率比较低及多核 CPU 的环境。

但是,我们需要留意 Survivor 区域的使用情况,避免这个区域空间不足引发不必要的 Full GC。我们可以通过参数设置来调整 Eden 和 Survivor 区的大小来满足具体的需求。

标记 - 压缩算法(Mark-Compact)

标记 - 压缩算法就是把要留下的对象统一放到一边,然后一次性清除掉另一边所有的空间。这个算法是为解决标记 - 清除算法和复制算法带来的空间碎片化问题而出现的。它的特别之处在于,它很好地利用了压缩手段,就像积木一样,把没用的方块移走,留下可利用的空间。

这样就能有效地改进内存的使用,节省内存资源并提高使用效率。跟复制算法不一样的地方在于,这个算法不需要分割内存,所以它不会浪费内存。

从 JDK 3.0 开始,新生代开始用复制算法,而老年代就用标记 - 清除 - 压缩技术,这是为了提高编程效率和准确性。这里的清除只是标记 - 压缩算法的一部分功能,这样在回收后,“压缩”这一步就留给了用户自己来决定,可以通过设置 JVM 参数 -XX:-UseCMSCompactAtFullCollection 来决定是否在 Full GC 后进行压缩整理。

image.png

它综合了标记清除和复制算法的优点,不仅减少了内存碎片,而且没有浪费太多内存空间,所以在许多 JVM 中都是 Full GC 的首选算法。

分代收集算法(Generational Collection)

最后,我们来聊下 JVM 中最经典的分代收集算法。在实际的生产环境中,每个 Object 都具备独特的生命周期,有的 Object 的生命周期很长,有的则很短。因此,在 GC 时考虑对象存活的年龄并进行相应地处理,有助于提高收集的效率。基于这个原理,Java 堆被划分成新生代和老年代两个区域。

分代收集算法主要分为 3 个阶段。

  1. 初始分配阶段

这个阶段新创建的对象首先在新生代的 Eden 区分配。当 Eden 区域达到极限的时候,Minor GC 会被激活,它会清除所有无用的对象,而那些能够持续存在的对象会被加入 Survivor 区域。一般这个阶段会采用复制算法进行。

  1. 对象晋升阶段

经过多次 Minor GC 后,仍然活跃的对象会被晋升到老年代。这个阶段采用标记 - 复制算法进行。Major GC 可以有效地处理 Java 堆里所有可能的变量。在这个过程中,我们可以通过标记 - 清除 - 整理的方式来实现对堆的有效管控。

  1. 内存调整

经过 Full GC 后,如果还是没办法满足内存需求,就会抛出 OutOfMemoryError。

分代收集算法可以对新创建的临时对象进行高效的内存回收。对于存活周期长的对象,由于发生 GC 的频率比较低,所以整体上可以提高内存回收的效率。但是如果老年代频繁执行 GC,应用的吞吐量会下降。在实际生产实践中,需要手动调整新生代和老年代的大小以及晋升阈值,才能达到理想的性能,但这也增加了使用复杂性。

image.png

此文章为9月Day20学习笔记,内容来源于极客时间《云时代JVM实战 》,强烈推荐该课程

相关文章

网友评论

      本文标题:JVM垃圾回收中的算法

      本文链接:https://www.haomeiwen.com/subject/hmsavdtx.html