Code前端首页关于Code前端联系我们

JVM 垃圾回收算法示意图

terry 2年前 (2023-09-27) 阅读数 77 #数据结构与算法
  • JVM 的作用
  • 什么是 Java 堆

因为我们讨论的是 JVM 的 Java 堆垃圾回收。为了突出核心,我会跳过一些与本文无关的其他内容

众所周知,Java堆存储的是对象实例,而Java堆的大小是有限的,所以我们只能释放一些垃圾已经用完的,无法再从内存中访问的对象,就像JVM帮助我们手动在类似于C++的代码中添加一行一样。自由句子行为

不过,这些垃圾如何回收并不重要。如果你现在不知道也没关系。我们一会儿会讲到

如何判断一个对象是不是垃圾对象

了解具体的GC(垃圾回收)算法之前我们先了解JVM是如何判断一个对象是垃圾对象的
顾名思义,垃圾对象是没有价值的对象。更准确的说,它是一个没有被处理过的对象,也就是它没有其他对象的Reference,这就引出了我们第一个考虑方案:引用计数法

引用计数法

原理原理这个算法是,每次有另一个对象创建对A对象的引用,那么A对象的引用计数值就+1。相反,每当一个对象对A对象的引用变得无效时,A对象的引用计数的值为-1。当A对象的引用编号值为0时,则被标记。对于垃圾物品

这个算法看起来非常不错。熟悉C++的人应该知道,C++智能指针也有类似的引用计数,但是这种看似“简单”的方法却无法判断一个对象是否存在。我们来看下面的场景:

JVM垃圾内存回收算法图解

在这个场景中,对象 A 引用了对象 B,对象 B 也引用了对象 A,所以这两个对象的引用计数不是 0,而是 A。两个对象 B 和 B 显然没有外部对象引用,就像大海中两个相邻的独立岛屿一样。尽管它们相互依存,但仍然是孤岛。其他人无法打通,而且由于参考数不为0,所以也是一座孤岛。不能算是垃圾。如果JVM有大量这样的垃圾对象,最终会频繁抛出OOM异常,导致系统频繁崩溃

总之,如果有人问为什么JVM不使用引用计数来评估垃圾对象。 ,只要记住这句话:引用计数方法并没有解决对象的循环依赖问题

可访问性分析方法

引用计数方法已经很接近结果了,但问题是为什么每个对象来借用的时候你需要添加到引用计数1。就像有人敲门时开门一样,我们应该只给我们认识的重要的人开门。换句话说,只有当我们想到重要的事情时才应该开门。数值+1

但这还不够,因为一个重要的对象引用就够了,没必要给每个引用都+1,所以我们可以将参考计算方法优化为以下形式:

在对象上设置标记。每当引用“重要项目”时,请将此标志设置为 true。如果没有“重要对象”可引用,则将该标志设置为 true。设置为 false 且标记为 false 的对象都是垃圾对象

这就是可达性分析方法的雏形。我们可以继续进行修复。我们不必主动标记对象,只需等待垃圾回收来找到这些对象即可。 “重要的项目”,然后从它们开始,将我们找到的所有项目标记为非垃圾项目,其余的自然是垃圾项目

我们将上面的“重要项目”命名为GC Roots,这给了我们最终的概念可达性分析算法:

垃圾收集创建中的根节点,俄语称为GC。从 GC Roots 开始,无法到达的对象被标记为垃圾对象

可用作 GC Roots 的区域有:

  • 虚拟机堆栈的栈帧中的局部变量表
  • 引用。通过方法作用域中的类属性和常量
  • 本地方法栈中引用的对象换句话说,GC根是本地变量,方法

    垃圾回收算法

    中的类属性和常量已经达到了本文的重点。刚才我们分析了如何判断一个对象是否是垃圾对象。接下来重点分析一下如何回收这些垃圾桶

    Mark-clear算法

    Mark-clear很容易理解。该算法有两个过程,标记过程和清除过程。标记过程中采用了上述可达性分析方法。标记所有非垃圾对象,并通过清理过程清理它们

    例如,我们现在有一个像这样的 Java 堆,所有垃圾对象都使用可达性分析进行标记(橙色表示蓝色的是常规对象):

    JVM垃圾内存回收算法图解

    然后我们通过清理阶段进行清理,结果如下:

    JVM垃圾内存回收算法图解

    你发现什么问题了吗?是的,清理后的状态是不连续的,也就是说,整个算法最大的缺陷是:

    • 会出现大量的状态碎片。当需要分配大对象时,会触发FGC,非常消耗性能

    这就引入了FGC的概念。为了避免偏离主题,本文暂时不深入讨论。你需要知道的是,垃圾收集分为YGC(年轻代垃圾收集)和FGC(完整垃圾收集)。 YGC可以理解为扫地。把垃圾拿出来,把 FGC 当作家里的一般大扫除

    复制算法

    复制算法将 Java 堆分为两个区域,一次只使用其中一个区域。当垃圾收集时,将所有标记的对象(GC Roots 可到达的对象,除了垃圾桶之外)复制到另一个区域并清理它们。清理完成后,切换两个区域的可用性

    这种方法是因为每次只需要一整块。只需将它们一起删除即可,而不是一一删除。同时可以保证第二区域的连续,也解决了空间碎片的问题

    我们再看一遍整个过程

    1。首先我们有两个部分区域S1和S2,灰色标记的区域是当前激活且可用的区域:

    JVM垃圾内存回收算法图解

    2。标记Java堆中的对象,其中蓝色的是GC Roots可达对象,其余的是垃圾对象:

    JVM垃圾内存回收算法图解

    3。接下来,将所有可用对象复制到另一个区域:

    JVM垃圾内存回收算法图解

    4。删除原来区域的所有内容,激活第二个区域

    JVM垃圾内存回收算法图解

    这种方法的优缺点也很明显:

    • 优点:解决空间不连续问题
    • 缺点:空间利用率低(只有原来的一半)空间利用时间)

    为了解决这个缺点,下面的算法

    优化了复制算法

    为什么不给它起个别的名字呢,其实是因为这个算法也叫复制算法。更准确地说,刚刚介绍的,只是一个优化算法的雏形。使用上面的复制算法的虚拟机是不存在的,所以接下来我想讲讲真正的复制算法

    这个算法的思想和我刚才讲的是一样的,只不过这个算法把内存分成了三个区域:1个Eden区和2个Survivor区,其中Eden区占80%

    两个生存区可以理解为刚才提到的两个区域S1和S2。我们每个人当前都只有整个伊甸园区域和一个幸存者区域在使用。整个算法流程可以简单概括如下:

    1。垃圾收集完毕后,立即将Eden+Survivor区的保存项目复制到另一个Survivor区

    2区。清理Eden区和使用过的Survivor区的所有物体

    3。改变两个幸存者的激活模式

    文字描述比较抽象,我们看一下图片的形状:

    1。我们有以下 Java 集群,其中灰色 Survivor 区域处于活动状态

    JVM垃圾内存回收算法图解

    2。标记所有 GC Roots 可到达的项目(蓝色标记)

    JVM垃圾内存回收算法图解

    3。将所有标记的项目复制到另一个幸存者区域

    JVM垃圾内存回收算法图解

    4。清理Eden区和激活的Survivor区的所有对象,然后切换两个区域的激活状态

    JVM垃圾内存回收算法图解

    上面就是复制算法的整个过程,有人可能会问为什么Survivor区这么小,就不怕不适合吗?事实上,平均而言,每次垃圾收集大约有 98% 的物品被回收。也就是说,我们完全可以保证大多数情况下剩余物品都会低于10%,将它们放置在Survivor区不会有任何问题。 。当然,Survivor区不足的问题也会出现。目前需要依赖其他内存来提供备份,GC复制算法解决了这个问题)

  • 复制算法无法处理长期存在的数据,只能频繁复制复制不必要地消耗资源(重点)

字符排列算法

这个算法可以说是专门针对对象存活率较高的程序而设计的。具体过程如下:

1。当GC发生时,将所有标记的存活对象移动到内存的另一端

2.移动后,清理移动边界外的所有物体

相信大家了解了前面的算法后,这个算法也很容易理解了。我就不画图举例说明了。 :

问题:如果表的长度为n,我们要保留所有10以下的数字,并删除剩余的数字
解决方案:可以遍历数据,将10以下的所有数字放入表左side 该方法的优点也很明显:

  • 实现简单,执行速度快
  • 对于复制算法处理不好的长寿命数据,标记压缩算法可能会决定不组织。这些对象
  • 不存在空间碎片问题,但仍然存在 缺点:
    • 如果堆内存较小,算法的速度会降低
    • 需要多次访问类型数据和指针字段
    • 新存储转发地址需要额外的空间,导致容量减少
    • 不适合并发收集器

    分代收集算法 ♷ 别担心,我们不是尚未完成的是最后一代拆分 Java 堆的收集算法。有两个区域:

    • 年轻代:存储短命的对象,即存活率较低的对象。大多数对象在 GC 后都会被回收
    • 老年代:存储存活率较高的对象

    可以看出,分代收集算法根据 GC 后对象的存活率将 Java 堆划分为两个区域。在不同的领域使用不​​同的算法可以“扬长避短”,尽可能地提高垃圾收集。回收效率

    • 针对年轻代来去的性质,我们使用复制算法
    • 针对老年代存活率高的性质,我们使用标签收集算法

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门