Jvm内存回收终极奥义:垃圾收集器

前言:

之前在一个java对象的死亡证明我们讲了一个对象是如何被判定为死亡的,并在之后的 Jvm内存回收终极奥义:垃圾收集算法里详细地介绍了目前较为流行的四种垃圾回收算法,后来各大虚拟机充分验证了这四种算法的可行性,所谓有需求就会有市场,本着回收垃圾,掌握核心科技的强大信念,垃圾公司应运而生了(你确定不是在骂人?),垃圾公司一口气推出了覆盖各大场景的七种垃圾收集器供广大消费者选择,不费吹灰之力占领了百分之90的市场份额,公司成立一天,火速上市,而竞争者们则被安排去了非洲,故事到这里,JVM宇宙正式形成了。

是的,一本正经的胡说八道。

这七种产品分别为:

  • 单线程垃圾收集小王子:Serial收集器
  • 多线程垃圾收集小王子:ParNew 收集器
  • 吞吐量优先小王子:Parallel Scavenge收集器
  • 单线程垃圾收集老王子 :Serial Old收集器
  • 多线程垃圾收集老王子:ParNew Old 收集器
  • 垃圾收集器明日之星:CMS收集器
  • 来自未来的次世代收集器:G1收集器

凭着在垃圾回收领域的优秀表现,无数次拯救了内存危机,它们被后人称为垃圾回收者联盟

Serial收集器:

首先第一个上场的是Serial收集器,它是所有收集器里面年龄最大的收集器,在jdk1.3之前那物质贫瘠的时代,Serial收集器就像IE一样是唯一的选择,Serial的垃圾收集方式有点类似于我们平常的保洁阿姨,收集垃圾的时候,会先告诉在场的所有线程:

你,你,你,别动,刚扫完

Serial收集器工作的时候会暂停掉其他工作线程,直到它收集结束,这对很多应用是难以接受的,比如你再打一款游戏java荣耀,正准备团战呢,Serial收集器站出来说,弟弟们先往边上靠靠,我要来收集垃圾了,给你暂停个五分钟,谁受得了,下面是Serial收集器的运行过程,其中新生代使用的是复制算法,老生代使用标记-整理算法

读者看到这里肯定会说了,这垃圾收集器也太垃圾了吧,其实不然,Serial的专一(单线程)收集在只有一个内核的系统中因为不需要去和其他回收线程进行交互,反而效率更高一点。

ParNew 收集器:

ParNew 收集器呢,其实就是Serial收集器的多线程版本,其中用到的收集算法,Stop the word(停顿类型STW,暂停其他线程),对象分配规则,回收策略几乎都与Serial收集器一模一样,但是青出于蓝而胜于蓝,ParNew在实行垃圾回收的时候是采用多线程的,当然,这并不足以使它立足于java虚拟机世界的理由,它最大的优势就是和CMS关系特好,几乎是拜把子的兄弟,俩人合作起来默契十足,而这些,Serial 和 Parallel Scavenge收集器就只能相形见绌了,可见,一个好的队友是多么重要。ParNew的工作过程如下图所示:

你这图看着有点斜啊,大家不要在意这些细节

但是parNew收集器并不是说因为加了多线程就完爆Serial收集器了,当只有一个内核的时候,ParNew的效率就通常没有Serial收集器高,原因在于一个小房间通常一个人打扫起来比十个人扫起来快,因为没有互相沟通的成本,但随着内核个数的增加,ParNew的优势才会逐渐体现出来。

Parallel Scavenge收集器:

巧了,Parallel Scavenge是一个新生代收集器,和ParNew收集器在多线程,复制算法又几乎差不多,那他究竟在厉害在哪了,没错,是Idea,当别的垃圾收集器都在拼命减少用户线程停顿时间的时候,它另辟蹊径,选择了吞吐量优先。

PS:吞吐量 = 用户运行代码时间/(用户运行代码时间+垃圾收集器时间)

吞吐量越高意味着用户代码运行的时间就越高,这里引用深入理解jvm虚拟机中的原文,感觉我怎么说也没作者说得好。

停顿时间越短越适合需要与用户交互的程序,良好的相应速度能提升用户体验,而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

存在感有点低,这个垃圾收集器没有图,,,

Serial Old收集器:

Serial Old收集器是Serial收集器的老年代版本,它同样是单线程的收集器,使用标记-整理算法,它主要工作在Client模式,由于Parallel Scavenge收集器和Serial和cms收集器关系都不太好,所有它们两个成了好朋友,一个新生代,一个老年代,所以可以配合使用,第二个是作为CMS收集器的后备预案。它的工作图如下所示:

ParNew Old 收集器:

同样的,ParNew Old收集器则是ParNew收集器的老年代版本了,使用多线程标记-整理算法,由于上文我们说到,Parallel Scavenge收集器和CMS收集器尤其不合,所以如果一旦选择了Parallel Scavenge收集器作为新生代收集器的话,那也就意味着老年代收集器我们只能选Serial Old收集器,而Serial Old收集器属于单线程处理器,难免会有着性能上的问题,所以这就比较蛋疼了,简直就是捆绑销售,这一切直到ParNew Old 收集器的面世才得到解决,至此,Parallel Scavenge收集器+ParNew Old 收集器才算是真正的实现了吞吐量优先的至臻组合。

下图是ParNew Old 收集器运行示意图:

CMS收集器:

CMS收集器,全称Concurrent Mark Sweep 收集器,和大多数收集器类似,也是致力于缩短线程停顿时间的垃圾收集器。本身采用的是进阶版的标记-清除算法

主要分为四个流程:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中并发标记和并发清除这两个做真正工作的占据了整个垃圾回收过程的绝大多数时间。初始标记可以说是先来初略的看一眼谁需要标记,花不了多少时间,重新标记则是看看哪个被标记的改动了,也是很快就完成的。

执行流程如下:

虽然CMS收集器是今日之星收集器,但是也并非完全没有缺陷,其中有三个比较明显的问题。

  • 第一个就是CMS收集器使用的标记-清除算法会导致大量的空间碎片,原因我已经在上篇文章垃圾收集算法有说明,不了解的朋友可以去看一下。
  • 第二个则是对CPU资源非常敏感:大家想,流程图那么多线程,当CPU多的时候还能接受,我家底厚,你吃得多也差不多能养起,可是当CPU数量比较少的时候,就相当于一个母亲养一堆嗷嗷待哺的线程孩子,就会很吃力,相应的也会导致我们应用性能的下降。
  • 第三个就是没办法回收“浮动垃圾”:这个浮动垃圾是什么意思呢,大家这么理解,你在家扫地呢,你弟弟在旁边嗑瓜子,当你扫到你弟弟那边的时候,你边扫它边往你扫干净的地方仍,但是,虚拟机规定你一次只能扫一次,没法回去,所以你得等下一次扫的时候在处理这些垃圾。所以,CMS无法回收哪些线程在回收的过程中因为运行而产生的新垃圾,也就是浮动垃圾。

G1收集器:

G1收集器我称它为次世代垃圾收集器,一款来自未来的垃圾收集器,真实的讲,就是G1是当今垃圾收集器发展的最前沿的成果之一,但是,由于一些问题没有解决所以迟迟不能上市,直到JDK7才正式加入jvm虚拟机大家庭,JDK9才作为默认的垃圾回收器出现。

G1的出现不是工程师们觉得垃圾回收器太少再随便加个,根据我们以往的历史经验,一款新技术的诞生必定是为了弥补旧技术的不足,G1同样是为了弥补CMS垃圾收集器的一些问题而诞生的,相对于CMS垃圾回收器,G1的主要提升主要在如下几个方面:

  • G1在压缩空间方面有优势
  • G1通过将内存空间分成区域(Region)的方式避免内存碎片问题 Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  • G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  • G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  • G1会在Young GC中使用、而CMS只能在O区使用

而相较于其他的垃圾收集器而言,G1主要有如下四个特点:

  • 并行与并发:能更好的利用多CPU的优势。
  • 分代收集:这个就厉害了,强大到没队友,可以管理整个GC堆,一个人carry全内存。
  • 空间整合:薛定谔的垃圾回收算法,整体看是基于标记-整理算法实现的,局部看又像是基于复制算法实现的,但无论使用它们两个中的哪个,都几乎不会产生空间内存碎片。
  • 可预测的停顿:有点像AI,会建立可预测的停顿时间模型,可以有计划的避免在整个Java堆实现全区域的垃圾收集。

而G1是实现流程相对来说还是很复杂的,在这里我们就不多加叙述了,想要了解的同学可以去搜索相关资料。

和CMS相同,G1的垃圾回收过程也主要分为四个阶段:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

流程图如下:

总结:

本篇文章我们较为初略的说了说jvm七种垃圾收集器,各有所长,没有绝对的吊打,只有场景的选择。合适的场景用合适的垃圾收集器,才会使我们的应用性能得到改善。

我是韩数,关注我,有你好果子吃(哼)

点个赞在走哦。

等一下:

相关源码欢迎去我的github下载(欢迎star):

github.com/hanshuaikan…

猜你喜欢

转载自juejin.im/post/5d85d093f265da03e71b3080