快速入门JVM第三讲——你得知道啥是GC?

首先来看看以下四道面试题,你觉得自己能解决几道面试题。

  • JVM内存模型以及分区,需要详细到每个区放什么;
  • 堆里面的分区:Eden,survival(from/to),老年代(养老区),各自的特点;
  • GC的三种收集算法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方;
  • Minor GC与Full GC分别在什么时候发生。

相信看过我前面文章的同学,除了第三道面试题之外,其余的面试题都已了然于心,我在这里并不会过多赘述,至于第三道面试题,相信你读完本讲,在心中就知道答案了。说白了,本讲就是围绕这道面试题展开的!

GC是什么?

首先,我们得知道GC是什么?GC就是分代收集算法。
在这里插入图片描述

GC的作用区域

GC的作用区域如下图所示。
在这里插入图片描述

GC中的四大算法

GC算法的总体概述

先看下面这张图,接着我会比照着这张图来讲。
在这里插入图片描述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(Minor GC),一种是全局GC(Major GC or Full GC)。
在这里插入图片描述

详述四大算法

引用计数法

如何来理解引用计数法呢?可以这样来理解:我们可能会在程序中new出来一个一个的对象,它既然叫做引用计数法,那么就应该是这些个对象中都会有一个一个的计数器,即每一个对象都会有一个计数器,但凡有人引用了一个对象,那么这个对象的计数器里面的数字就自增。什么样的对象,JVM会把它回收掉呢?当该对象的计数器变为0的时候,没人用的时候,JVM就可以把它回收掉了。
在这里插入图片描述
引用计数法也有其缺点,它的缺点如下:
在这里插入图片描述
我们来看看第二个缺点,较难处理循环引用,也就是说引用计数法不适用于双向循环引用的案例,就像下面这样。
在这里插入图片描述
温馨提示:JVM的实现一般不采用这种方式了,也就是说它已经被淘汰了。

复制算法(Copying)

年轻代中使用的是Minor GC,这种GC算法采用的就是复制算法(Copying)。

原理

先看下下面这张图。
在这里插入图片描述
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old Generation中,也即一旦收集后,Eden是就变成空的了。
当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为了from区域,所以这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过参数-XX:MaxTenuringThreshold可以设置对象在新生代中存活的次数),这些对象就会成为老年代。
你看到这里,有没有什么感想呢?其实缓解OOM还有另外一种方式,即把新生区到养老区的这个门槛调高点,怎么调呢?通过-XX:MaxTenuringThreshold来设定参数继而就能调高点了,比如调到30岁,但一般很少有人去动这个参数,一般用默认的就行,都是通过扩展JVM的内存大小去调整。

优缺点

年轻代中的GC主要是复制算法(Copying)。HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(又分别叫from区和to区),默认比例为8:1:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面复制算法的第一个优点:它不会产生内存碎片
在这里插入图片描述
从上图中,我们还能发现复制算法的第二个优点:由于没有标记和清楚的过程,所以它的效率会更高。
在GC开始的时候,对象只会存在于Eden区和名为"from"的Survivor区,名为"to"的Survivor区是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到"to"区,而在"from"区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到"to"区域。经过这次GC后,Eden区和from区已经被清空。这个时候,“from"和"to"会交换他们的角色,也就是新的"to"就是上次GC前的"from”,新的"from"就是上次GC前的"to",不管怎样,都会保证名为"to"的Survivor区域是空的。Minor GC会一直重复这样的过程,直到"from"区被填满,"from"区被填满之后,会将所有对象移动到年老代中。
在这里插入图片描述
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的Eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。这里用一张动图来演示一下所谓的复制算法。
在这里插入图片描述
经过上面的讲述,你能总结出复制算法的缺点吗?复制算法的缺点有如下两点:

  1. 它浪费了一半的内存,这太要命了。怎么理解这一点呢?说白了,复制算法需要两片内存,这是有点太浪费了。如果说新生区存活率比较高,存活的对象比较多,那么在这块中多了一倍的内存,确实是有点受不了。但是别忘了这一点,复制算法使用的地方是新生区,而新生区的特点就是存活率比较低,就算是需要两倍的内存,那也不会占用太大的量,所以这就是为什么新生区中对应的幸存from区、幸存to区都是很小的一块,原因就在这儿,因为存活下来的对象确实很少,就算是两倍的内存,也是可以接受的;
  2. 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

标记清除算法(Mark-Sweep)

老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

原理

先看下下面这张图。
在这里插入图片描述
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记:从引用根节点开始标记所有被引用的对象。标记的过程其实就是遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象;
  • 清除:遍历整个堆,把未标记的对象清除。

用通俗的话来解释一下标记清除算法,就是在程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。这里用一张动图来演示一下所谓的标记清除算法。
在这里插入图片描述

优缺点

从下图不难看出标记清除算法的优缺点。
在这里插入图片描述
优点是不需要额外的空间。缺点也很显而易见,有如下两点:

  1. 首先,它的第一个缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。温馨提示:但凡JVM在做标记清除算法的时候,也就是Full GC的时候,功能一定要暂时停一下,即使是对用户的体验不太友好,因为功能暂时不能用了。
  2. 其次,它的第二个缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

总结一下标记清除算法的缺点,其实就是这样一句话:此算法需要暂停整个应用,会产生内存碎片。

标记压缩算法(Mark-Compact)

老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

原理

先看下下面这张图。
在这里插入图片描述
在整理压缩阶段,不再对未标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优缺点

标记压缩算法不仅可以弥补标记清除算法当中内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价,这应该就是其优点。标记压缩算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址,从效率上来说,标记压缩算法要低于复制算法。

标记清除压缩算法(Mark-Sweep-Compact)

标记清除压缩算法其实不算是GC中的一种,它只不过是标记清除算法和标记压缩算法的结合,也就是把标记清除算法和标记压缩算法混合到一块使用,多次清除之后才压缩。
在这里插入图片描述
真实情况下,在养老区都是这么混合着去使用标记清除算法和标记压缩算法。啥子意思呢?出现多次标记清除之后,这个内存空间就千疮百孔了,到处都是内存碎片,之后怎么做呢?统一做一次标记压缩就可以了。下面用一张动图来演示一下所谓的标记清除压缩算法。
在这里插入图片描述

总结

在这里插入图片描述
可以看出,从效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记压缩算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记清除算法多了一个整理内存的过程。难道就没有一种最优算法吗?猜猜看,答案是无,没有最好的算法,只有最合适的算法。
在这里插入图片描述
基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记压缩的混合实现。以HotSpot中的CMS回收器为例(只限于Java8),CMS是基于Mark-Sweep实现的,对于对象的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值