分享

new出来的对象怎么回收

 丰收书屋 2021-12-29

之前看过了垃圾回收算法的新生代GC,也是使用的一种比较浪费内存的复制算法,晚上看书又接着往下看了一点,

堆 = 新生代+老年代,但是要注意一点老年代不包括永久代(方法区),也就是说堆内存中只有新生代和老年代,而永久代是指的方法区。

之前介绍过新生代中的垃圾回收机制了,再来介绍一下老年代的垃圾回收机制里面使用到的算法。

新生代GC:MinorGC 之前介绍过了不说了,复制算法图解也比较清晰

老年代GC:FullGC 我们先说FullGC出现的原因吧,FullGC是老年代的GC,在新生代如果说存在的对象或者说新创建 出来的对象由于某些原因需要移动到老年代中,但是老年代中压根就没有这么大的内存空间去容纳这个对象, 那么就会引发一次FullGC,如果在执行完FullGC之后,还是没有办法给这些对象分配内存,那么凉了,该抛出异常了,异常类型就是OutOfMemoryError。

而FullGC使用的是和MinorGC不一样的算法,它使用的是标记清除算法,听名字,挺好理解的,来波图示解析一波。 深入了解JVM一书中的图示是这个样子的,

e69d1c3687dcda4d0c43028d9319b7a0.png

看名字的话是先标记,然后在删除。这也是也给最最基本的算法。 这个算法就是分两个步骤

  1. 标记(Mark)过程:找到所有的可以访问的对象,做个指定的标记。
  2. 清除(Swep)过程:遍历堆内存,把未标记的对象进行一个回收。

之前看一些文档上说,先标记,然后把没有标记的对象给回收掉,其实意思都差不多,但是在深入理解JVM一书中说到,首先标记出所有的需要回收的对象,在标记完成之后统一回收所有的被标记的对象, 其实我的理解和书中感觉有点不太一样,不过区别也不大。我说说我的理解吧。

在了解了这个之后,我们还得说一个概念,那就是GC Root,Root我们可以理解成一个根节点

就像这个样子

044cecb6a15240e2b329af525fe27ce8.png

上图中的a,b,c,d,就是活着的对象,如果说存在这引用,比如说b引用的a,那么a他就是属于活着的对象。 当我们老年代内存区中的有效的内存空间不够的时候,那么这时候整个世界都要安静下来了(stop the world),这时候就要开始准备进行垃圾回收了。

  • 标记:遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。就是我们图中所标记的a,b,c,d.
  • 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。 也就是说,如果内存不够,GC线程就会被触发然后将程序暂停,随后将依旧存活的对象标记一遍,最后再将堆中所有没被标记的对象全部清除掉,接下来便让程序继续恢复运行。 流程图就想这个样子的 初始下的老年代中的对象状态
5208b34b83ce4f07d7ea3d05904bc15f.png

这时候都是没有被标记的状态,接下来内存不够,GC线程停止,开始进行标记了

f580107c9d49bdd05830fd15b9494f13.png

按照根节点开始遍历 标记的abcdeh都是存活的对象,接下来开始标记。

06590cee91b2318d32f97af2bd8c451c.png

接下来就是清除数据了,这个就更加的简单了

c4431b73893614195c252f03a4981d7c.png

清楚完成之后还有就是把标记去除掉,可以下次进行标记清除的时候继续清除

ae6284f6f04dcf1c0180b9f682f5042a.png

这样标记清除就执行完毕了,剩下还有两个要说的地方, 一是在进行标记清楚算法的时候为什么要让程序停止,(stop the world)。 二是标记清除算法的优点和缺点又是什么?

(Stop the World)

程序停止其实可以理解,因为如果说不停止程序的话,我们在标记完成这个b对象之后,我们new出一个新的对象J,可以从B指向J,也就是说,这时候J应该是被标记的状态,但是实际情况肯定不是,这个对象在B标记完之后,马上都要结束了,我们又new出来一个对象,可想而知,他肯定是没有被标记的,所以在第二阶段进行清除的时候,这个苦命的J将会被清除掉, 那这样肯定是不符合我们的实际情况的。

你想呀这刚刚new出来的对象结果被清除了,忽然变成了空值,那就不符合我们的要求了。所以他会让程序先停止,然后不会再出现这种情况,然后进行开始标记阶段。

优缺点

首先我们可以先看缺点,他的缺点非常明显,

  • 因为他会递归遍历Root,这样的话 Stop the World的时间就比较长了,这样一直让人等待的滋味可不是那么好受。
  • 第二个就是这种清除方式清除出来的内存空间是不连续的,你看这个图
044cecb6a15240e2b329af525fe27ce8.png

死亡的上下分成了2部分,是不连续的,这样给JVM又造成了一种额外的负担,他需要去维持一个内存的空闲列表,如果说我们在这时候去new一个数组,你想想他去找这个连续的内存空间的话,是不是就要困难很多呢?

他的优点也有,

  1. 比如说不会出现循环引用, 我们可以想想 两个类 互相引用,A中newB,B中newA,那这样岂不是a.b=b ,b.a = a ,是吧 ,而标记清除算法在走完了之后,是可以回收a,和b的,因为他是从根元素开始遍历标记,也就是从ab开始,那么单一的a和单一的b就是没有被标记的,所以,这样就避免了循环引用的问题 2.还有一点感觉没啥区别,都是内存不够的时候才进行的引用。这没啥说的。

标记–整理算法

而因为标记–清除算法会导致内存分配都出现了各种不均匀的空间,这时候就有了另外的一种算法,直接把那些存活的对象标记出来,然后给他怼到内存空间边界,然后剩下的直接全给他清除了。这方法图解看的一清二楚,剩下的都是和标记清除算法一样的,好像没啥解释的,直接上图。

7b55956ea92e42adbfce9c7892eecbf7.png

书中你看就是把存活的都给怼到内存空间的上边,你也可以随便的理解成上下左右都ok。

以上就是堆内存中的老年代的两种垃圾回收算法了,如果有不合适的,希望大佬可以指正,一起讨论一下。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多