GC的四种清理算法

1.标记-清除:

这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:

  1. 效率不高,标记和清除的效率都很低;
  2. 会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

2.复制算法:

为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)

3.标记-整理

该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

4.分代收集

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。


内存清除算法

1.标记-清除:

标记 GC roots可达的对象,清理掉没有被标记的对象。

做法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是清除。

  1. 标记:遍历所有的GC Roots,然后将GC Roots可达的对象标记为存活的对象。
  2. 清除:清除的过程将遍历对中所有的对象,将没有标记的对象全部清除掉。

当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。

标记的时候为什么有停止程序运行呢?
假设我们刚标记完图中最右边的那个对象,暂且记为A,结果此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0,因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。如此一来,不难想象结果,GC线程将会导致程序无法正常工作。上面的结果当然令人无法接受,我们刚new了一个对象,结果经过一次GC,忽然变成null了,这还怎么玩?

缺点:

  1. 效率较低(递归,遍历整个堆的对象)而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
  2. 清理出来的内存空间不是连续的(死亡对象都是随机出现在内存的各个角落的)。再分配数组对象的时候,寻找连续的内存空间不太好找。

2.复制算法:

将内存分为两块

做法:当内存空间耗尽时,暂停程序运行,开启复制算法GC线程。将活动区间(内存)的存活对象复制到空闲的那一块内存区域,并且严格的按照内存地址依次排列,与此同时GC线程将更新存活对象的内存引用地址指向新的内存地址。 将标记为死亡对象一次清除掉。之后(活动)的那一块内存区域变为空闲,空闲的变为忙。

缺点:
1. 浪费了一半内存。
2. 如果对象的存活率很高,假设为100%存活,那么就需要将所有对象都复制一遍,并且将所有引用地址复制一遍。复制这一工作所话费的时间,在对象存活率达到一定程度时,(复制所用时间)将会变的不可忽视。

总结:要想使用复制算法,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%的内存浪费。

3.标记-整理算法:

做法:

  1. 和标记-清除算法一样,标记出存活的对象。
  2. 按照内存地址依次排列,而未被标记的内存会被清理掉。

不难看出,标记-整理算法不仅可以弥补 标记-清除 算法中内存区要分散的缺点,也消除了复制算法中内存减半的高额代价。(但是从效率上讲,标记-整理算法要低于复制算法)

算法总结:

  1. 都是基于根搜索算法GC Roots)的来判断一个对象是否应该被回收
  2. GC线程开启时,或者说GC过程开始时,它们都要暂停服务
  3. 效率:
    • 复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
    • 内存整齐度:复制算法 = 标记/整理算法 > 标记/清除算法。
    • 内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法。

结束语

到此我们已经将三个算法了解清楚了,可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程。
难道就没有一种最优算法吗?
当然是没有的,这个世界是公平的,任何东西都有两面性,试想一下,你怎么可能找到一个又漂亮又勤快又有钱又通情达理,性格又合适,家境也合适,身高长相等等等等都合适的女人?就算你找到了,至少有一点这个女人也肯定不满足,那就是多半不会恰巧又爱上了与LZ相似的各位苦逼猿友们。你是不是想说你比LZ强太多了,那LZ只想对你说,高富帅是不会爬在电脑前看技术文章的,0.0。
但是古人就是给力,古人说了,找媳妇不一定要找最好的,而是要找最合适的,听完这句话,瞬间感觉世界美好了许多。
算法也是一样的,没有最好的算法,只有最合适的算法。

既然这三种算法都各有缺陷,高人们自然不会容许这种情况发生。因此,高人们提出可以根据对象的不同特性,使用不同的算法处理,类似于萝卜白菜各有所爱的原理。于是奇迹发生了,高人们终于找到了GC算法中的神级算法—–分代搜集算法。

4.!分代搜集算法:

本质 属于前三种算法的实际应用 新生代,老年代,永久代

新生代:朝生夕灭,存活时间短。eg:某一个方法的局部变量,循环内的临时变量等等。
老年代:生存时间长,但总会死亡。eg:缓存对象,数据库连接对象,单例对象等等。
永久代:几乎一直不灭。eg:String池中的对象,加载过的类信息。

java堆:新生代,老年代 方法区(永久代):

使用这样的方式,我们只浪费了10%的内存,这个是可以接受的,因为我们换来了内存的整齐排列与GC速度。第二点是,这个策略的前提是,每次存活的对象占用的内存不能超过这10%的大小,一旦超过,多出的对象将无法复制

为了解决上面的意外情况,也就是存活对象占用的内存太大时的情况,高手们将JAVA堆分成两部分来处理,上述三个区域则是第一部分,称为新生代或者年轻代。而余下的一部分,专门存放老不死对象的则称为年老代

JVM在进行GC时,并非每次都对上面三个区域一起回收,大部分回收的是新生代。因此GC按照回收的区域又分为两种:普通GC,全局GC
普通GC:只针对新生代区域的GC
全局GC:针对老年代的GC,偶尔伴随新生代的GC以及对永久带的GC

由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC

你可能感兴趣的:(算法)