您好,欢迎来到爱够旅游网。
搜索
您的当前位置:首页JVM9:STW:stop the world,什么时候会垃圾回收?垃圾收集算法:标记清除算法、标记复制算法、标记整理算法;清除算法的整理顺序:任意顺序,滑动顺序;什么是分代收集算法?

JVM9:STW:stop the world,什么时候会垃圾回收?垃圾收集算法:标记清除算法、标记复制算法、标记整理算法;清除算法的整理顺序:任意顺序,滑动顺序;什么是分代收集算法?

来源:爱够旅游网

STW:stop the world

STW(stop the world),让世界停止。就是GC线程运行的时候,停掉业务线程的全世界。而业务线程中断,这个代价是非常巨大的。

比如玩游戏,10s卡1s,那游戏体验是很糟糕的,所以很多开发者,在知道Java有这个毛病之后,就放弃了Java,转行去了C和C++,那么Java发展到现在能不能解决这个问题?答案是依然不能,每个垃圾收集器都会或多或少的去暂停应用线程,也就是所说的业务线程,只不过暂停时间的长短问题。那么如果把一次暂停的时间变得很短,比如缩短到一次网络延迟20ms,那就可以容忍了。

什么时候会垃圾回收?

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。

(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()

垃圾收集算法

已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,下面介绍常见的垃圾回收算法。

标记-清除(Mark-Sweep)

  • 标记
    找出内存中需要回收的对象,并且把它们标记出来。 此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。

  • 清除
    清除掉被标记需要回收的对象,释放出对应的内存空间。

缺点

标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,因为需要递归遍历全堆对象,效率不高,尤其还需要全程STW,效率会更慢,用户体验很差
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;同时也是因为内存空间的不连续,那么怎么存放?JVM不得不维护一个空闲的列表,比如进来一个对象,存放之前要先判断这个对象是不是能放在这个空间下,包括在给大的数组对象、字符串对象时,寻找连续的空间时,会不太好找。

那么这个算法这么垃圾,为什么还要了解,因为一个算法有缺陷,必然会有很多人想方设法对它进行一个完善,那么接下来一种算法横空出世——标记复制清除算法。

标记-复制(Mark-Copying)

缺点

空间利用率降低。标记清除复制算法,是典型的空间换时间的算法,每次只是用其中的一块,另一块保持空着的状态,浪费了一半内存,这代价依然很大。GC本来就是穷人算法,穷人算法就要有穷人算法的觉悟,怎么可以浪费内存呢?所以不可能所有内存空间都使用复制算法,即使它效率快。JVM中也是只有一部分地方用到了复制算法,那其它的空间用到的是什么算法呢?

内存空间的不连续既然可以进行整理,那我们就给他时间让它整理呗,于是就有了——标记清除整理算法。

标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。

那么标记清除算法的整理又是怎么整理的呢?
能够影响整理的因素有很多,最重要的一个因素是顺序。因为整理过后,还不满意,比如相邻的引用对象想放在一起。

清除算法的整理顺序

那么既然顺序最重要,整理算法可以按照顺序分为三类:

  • 任意顺序:瞎整,只要满足内存排布是连续的即可,简单高效。实现简单,执行速度非常快,但是可能将原本相邻的对象打乱到不同的区域。致命缺陷:只能处理同样大小的数据,一旦对象的大小不确定,需要增加额外的判断逻辑
  • 线性顺序:把相邻的,相关联的对象整理在一块就可以,这样关联对象寻找的时间会变短(内存是不需要寻道的,但是如果两个数据快离得近同样会节省时间,这里只是举个例子),不去管那些空间碎片,明显不适合用在JVM中。
  • 滑动顺序:所有的对象滑动到前面,剩余的空间回收掉

那么顺着这几种思想,又可以衍生出非常多的一个算法,代码需要用能够创建指针、标记指针的语言来写,比如C。Java是不能的。

任意顺序——双指针算法

一般两个指针要么再同一个方向,要么在相反的方向,运行速度相同,直到两根指针相撞,这种叫对撞指针,对撞指针是为了解决所谓的长的字符串,所谓的数组,或者是内存空间,这种非环的链式结构

同向的双指针算法还有一种特别的算法,一个指针移动的快,一个移动的满,那么最终会相遇,如果相遇了,就表示有环。这种一快一慢两个同向指针的叫做快慢算法,快慢算法一般是为了解决所谓的链表环的问题

对撞指针如何做整理算法的呢(以两根指针不同方向为例)?

头指针只找空闲内存单元,尾指针只找可达对象。需要两次遍历操作:第一次遍历只是复制,不会执行任何其它的操作;第二次操作才回去执行更新引用。
第一次遍历:

  • 标记复制阶段:两个指针从堆的两端分别开始移动。一个指针(通常称为“头指针”或“前指针”)从堆的开始位置向后移动,另一个指针(通常称为“尾指针”或“后指针”)从堆的末尾向前移动。

    • 头指针只找内存空闲的单元
    • 尾指针只找可达对象(存活对象)

    每当指针移动到自己的下一个位置时会进行等待另一个指针;如头指针找到了内存空闲单元,会等尾指针找到一个可达对象(存活对象),这个时候会"",然后将这个对象复制到头指针的空闲内存单元

  • 指针相撞:两个指针移动,直到相撞,代表走完了这段线性表的所有数据。相撞的位置前面的空间是连续的可达对象,后面的空间是复制过的对象及空闲的内存单元,然后整体清除掉相撞位置后面的空间。

第二次遍历:在垃圾回收的过程中,特别是在使用复制算法或标记-整理算法进行内存整理后,第二次遍历通常是用来检查和,确保它们指向正确的内存位置。
头指针从相撞的位置开始往前遍历(遍历的是对象),开始比对对象的位置是否发生过改变,如果改变了,那么这个对象就要。

滑动顺序——三次遍历算法

  • 第一次遍历:估算位置,计算可达对象将要迁移的位置。两个头指针会一起向堆尾移动,一个只找空闲内存单元,找到停下来;另一个找可达对象,找到可达对象之后,该指针会在这个对象对应的位置分配一个额外的空间(数据是存在对象里的),标记这个内存块会迁移到哪个位置(此时只记录,不迁移),然后依此类推
  • 第二次遍历:修改引用关系。
  • 第三次遍历:真正移动,变换对象地址。

滑动顺序——单次遍历算法

它会用一张额外的表去记录,如果说很多对象都在这个表里去记录,那么内存肯定是经不起这么折腾的,于是又有一种方案,就是将内存分成大小相同的块,然后再表里面记录两个东西:

然后直接迁移对象和更新引用。

总结

这么多的算法都是一些概念,具体的实现会根据不同的开发者、不同公司的垃圾收集器、以及垃圾收集器的不同版本会有细微的差别。不光整理算法,标记清除算法也会有很多衍生的规则,比如清除之后,剩余的空闲空间在不进行整理的时候,势必会有一个空闲列表去维护空闲空间的信息,这样对象来了之后,会去判断是否放得下,然后给出合适的内存位置,那么这种空间分配的方式也会有很多种算法。

什么是分代收集算法?

上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?
对象会有非常对多的属性,其中有一个是生命周期,生命周期长的放在Old区,生命周期短的放在Young区。

Young区:复制算法。Young区的对象,生命周期都比较短,98%的对象都是朝生夕死的。选用复制算法效率高(因为Young区会频繁的创建对象),不管内存浪费不浪费(因为98%的对象都是朝生夕死的),但是也不可能整个young区都选用复制算法,因为这样空间浪费太大了,所以只针对存活的2%的对象采用复制算法(Survivor区),其它正常回收就好(Eden区)。
Old区:标记清除或标记整理。Old区的对象,生命周期都比较长,生命周期长也就是说,可以不必在乎效率,因为两次GC的间隔肯定会很长,也就是说有时间去慢慢的整理,因此用标记整理算法,如果说能够容忍空间碎片,那么还可以用标记清除算法。

针对不同对象的生命周期划分不同的存放区域,针对不同存放区域的特性采用适合的垃圾回收策略,这就是经典的分代收集算法。

汇总

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- igbc.cn 版权所有 湘ICP备2023023988号-5

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务