G1垃圾回收器

心得

   每个公司应该每周、月、年都有例会,只是不同的时间对应的会议也不同,像我们公司每个月不仅仅有月会,并且还有个人演讲,恰好本月的演讲人是我,所以我就写了一遍GC的文章进行演讲,总体来说经验欠缺些,只是对概念以及逻辑理解到位,但是实战经验并不充足,毕竟所有的理论都会败给实践的,好了,闲话不多讲了,以下则是我演讲的一部分,因为有些演讲的内容不适合写作于文档,所以可能少了几个目录点(反正你们也不清楚整体目录~~)。
在这里插入图片描述

1. 什么是垃圾回收器

1.1 什么是垃圾

​ ​ ​ 描述:说到垃圾,可能大家最初幻想到的是可回收垃圾、有害垃圾、干垃圾、湿垃圾等…但我们今天要讲的确实程序中的垃圾,而此垃圾彼垃圾是不同的,一个是现实垃圾,一个是程序垃圾,虽然不同但他们有一个相同点,都是垃圾,那么具体什么是程序垃圾呢?其实这个垃圾是很抽象的,比如在计算机中的话,我们可以把一些不用的文件或者未卸载掉的一些吃灰的软件或软件的一些残留数据等,我们都可以视为垃圾,当然这仅仅只是计算机中的看法,如果要是在程序中,高级语言中(Java),垃圾的概念则为:当一个对象没有任何引用指针的时候,那么该对象就会被视为垃圾对象。
在这里插入图片描述在这里插入图片描述

1.2 为什么需要垃圾回收器

​​ ​ ​ 描述:假设场景1:生活无微不至,生活很美好,但是我们生活的同时,也制造的垃圾物,我们大脑默认的思路为,有垃圾优先仍到垃圾篓,而垃圾篓装满之后,把垃圾篓放到集体垃圾箱中,而集体垃圾箱爆满之后,集体垃圾箱会被统一运送到垃圾回收场,而垃圾回收场则会将这些垃圾进行清理、处理,但如果我们没有以上系列步骤,当有垃圾的时候直接任意扔掉,可想而知,中国十几亿人口如果每人都这样的形式处理垃圾的话,那么相信不过多久,自然灾害则会进行反击,地球也会被破坏,我们也要迁移星球,然而迁移星球并不能解决根源,我们还是会制造无限的垃圾,因为我们只制造,不处理不清理,那垃圾回随着时间的推移,变的越来越多,变的越来越难处理,所以如果我们没有垃圾回收的概念,则会导致很严重的后果。假设场景2:在Java中,每当我们新建对象的时候,这个对象都会进行存储,假设,如果我们的程序只存不删的话,最终可能会内存占满,程序瓦解,这是很现实的问题,程序可是固定的,遇到难题直接抛给你,又或者说redis,如果redis没有一些回收策略(例如:淘汰最少使用、随机淘汰、定时删除等)的话,那久而久之,服务器压力紧张、服务瘫痪、程序宕机都是很正常的,所以无论是生活中还是程序中我们都无法脱离垃圾回收概念以及实现,毕竟先天上飞的理念,后地上跑的实现,而java针对于垃圾回收也做出了有效的解决方案,并且不断迭代升级到的垃圾回收器,从最初的:串行垃圾回收器、并行垃圾回收器、并发垃圾回收器等各种方式各个类型的垃圾回收器不断推出,为的就是程序的高效性能。
在这里插入图片描述
在这里插入图片描述

1.3 垃圾回收器怎么触发

​​ ​ ​ 描述:每个垃圾回收器都是有触发机制的,例如年轻代系列的垃圾回收器都是当eden区域达到多少阈值的时候来进行垃圾清理的,而执行垃圾回收器的则是执行引擎,这只是抽象上来讲,具体讲的话,要要看哪个代里面的垃圾回收器机制,各个垃圾回收器都有一个阈值,比如内存占比高于xx%的时候就会触发,或是低于xx%就不会触发等,而触发之后的流程步骤也是根据当前垃圾回收器的策略进行,并不是所有的垃圾回收器的策略是一致的,思想相同,但对于细节方面的作法不同,不断优化、不断尝试。

2. 垃圾回收器历史

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.1 Serial

​​ ​ ​ 描述:Serial串行垃圾回收器,年轻代中采用的复制算法、老年代(Serial Old)中采用的是标记-清除算法并且是单线程执行,JDK最早版本的时候就存在了属于年龄最久的垃圾回收器,由于年轻代和老年代都是单线程,所以它的工作模式是和用户线程冲突的,当serial工作的时候必须将用户线程停止(STW)直到serial扫描并且收集完成之后才会恢复正常用户线程,Serial收集器适合大多数对停顿时间要求不高和在客户端运行的应用,由于它是属于垃圾回收器中最久的一款并且当时的技术和现在的技术不一致,所以serial慢慢的也被淘汰了、慢慢的消失在了广大群体中。
在这里插入图片描述

2.2 ParNew

​​ ​ ​ 描述:ParNew收集器,全称Parallel New(并行垃圾回收器)多线程方式,年轻代使用的是复制算法,老年代(Serial Old)采用是标记-清除算法,年轻代工作默认模式为:垃圾回收时触发stw用户线程停止,开始多线程执行扫描以及垃圾回收,而老年代工作模式为:垃圾回收时触发stw用户线程停止,开始单线程扫描以及垃圾回收,JDK1.4之前与Serial Old进行使用,JDK1.5d时候和CMS(并发垃圾回收器)结合使用。
在这里插入图片描述

2.3 Parallel Scavenge

​​ ​ ​ 描述:Parallel Scavenge收集器,见名之意,也属于并行垃圾回收器,和Parallel Old收集器搭配使用,年轻代是Parallel Scavenge GC采用的是复制算法,而老年代使用的Parallel Old GC采用的是标记-压缩算法,两者为一体,可拆分,但两者相互兼顾,使用参数设置年轻代为Parallel Scavenge的时候,老年代会自动采用Parallel Old,除非手动设置老年代GC,否则Parallel Scavenge和Parallel Old默认都是相互兼顾的。
在这里插入图片描述

2.4 Serial Old

​​ ​ ​ 描述:Serial Old(年老代垃圾回收器),采用的是标记-压缩算法,可以和Serial、Parallel Scavenge整合,并且还是CMS的备选方案,毕竟CMS属于并发垃圾回收器,如果没有备选方案的话,会严重影响用户体验。
在这里插入图片描述

2.5 Parallel Old

​​ ​ ​ 描述:Parallel Old(老年代并行垃圾回收器),采用标记-压缩算法,与Parallel Scavenge Old整合。JDK1.6开始提供,这里有个梗跟大家分享下,就是说,在Parallel Old还没有提供前,使用Parallel Scavenge的时候也只能被迫选择和Serial Old整合使用,即使Parallel Scavenge 高效、高吞吐,但是老年代中的Serial Old和它却不是无缝衔接,所以整体效果一般,而当Parallel Old横空出世之后,Parallel Scavenge 和Parallel Old如同英雄见美人、纣王遇妲己一样,衔接在了一起,并且实现了真正意义上的高吞吐。
在这里插入图片描述

2.6 CMS

​​ ​ ​ 描述:CMS(老年代并发垃圾回收器),采用的是标记-清除,也算是并发垃圾回收器的鼻祖(首批思想或技术落地实现)了,和Serial、ParNew 垃圾回收器整合,整体流程:初始标记–并发标记–重新标记—并发清除等。
在这里插入图片描述

2.7 G1

​​ ​ ​ 描述:横空出世我的天啊,G1(全称:Garbage-First,寓意:最快垃圾收集器),G1是当今收集器技术发展的最前沿成果之一,属于并发垃圾回收器,它的出现毫不夸张的讲,就是替代CMS的,而G1的优点分为以下几种。

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式取处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-压缩”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java的垃圾收集器的特征了。虽然CMS要被G1替代,但是收集思想仍旧被G1采纳,并且在基础之上进行了升级。】
  • G1的收集步骤:初始标记—并发标记–重新标记–优先回收
    在这里插入图片描述

2.8 吞吐量和低延迟

​​ ​ ​ 对于吞吐量和低延迟,它们两个的缘分就类似于CAP原则(三者不可兼顾,只能人选其二)一样,而吞吐量和低延迟两者不可兼顾,只能任选其一,当然如果非要两者兼顾的话,那么两个都会很一般,如果了解过zk和eureka的区别的话就会清楚为什么cap只能任选其二,如果强制性兼顾3的话,那么效果是不佳的,而垃圾回收器的设计也针对于这两个区域进行开展,比如G1比较看重吞吐量,而ZGC比较看重低延迟,两个垃圾回收器的本意都是回收垃圾,但是实现细节却不同,对于G1的话,该文章会描述G1,但对于ZGC的话,有兴趣可以去官方文档查看ZGC的说明。

​​ ​ ​ 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
在这里插入图片描述

3. 垃圾收集器系列算法

3.1 标记–清除

​​​ ​ ​ 描述:标记清除算法,标记,顾名思义,对某些事物标记,而在我们程序中垃圾回收器中,则就是用来标记非垃圾对象或垃圾对象的,由于Java采用的是可达性分析算法,所以在Java中当被对象被标记的话则代表可达性对象,而可达性属性则记录在对象头信息中(对象头信息存储在元空间或者永久代中,具体名称基于java版本进行称呼),被标记过的对象,代表该对象有引用或被引用,而没有被标记过的对象,则代表该对象没有被其他的对象所引用或者自己没有引用GCRoots根,所以最终等待它的结果则就是被当成程序垃圾清理,当然,当我们标记完成之后,并不是第一时间就进行清除,而是当内存达到一定的阈值的时候才会进行清理,并且清理也不是说把这块区域删掉,而是将这块区域中的对象进行删除,等待下一个新的对象分配的时候可以会分配到该区域中,但是“标记-清楚”算法会造成内存碎片,所以这是一个比较头疼的事情。 --等闲列表分配法

3.2 标记–压缩

​​​ ​ ​ 描述: 标记压缩算法,和标记清楚算法同理,基础之上进行优化、改造、升级等,解决的问题无异于内存碎片了,有效的解决了内存碎片,每次垃圾清理完成之后,都会将内存布局整治。 —指针碰撞分配法

3.3 复制算法

​​​ ​ ​ 描述:JDK1.8中年轻代的survivor区域采用的则就是复制算法以及JDK1.9(包含1.9)之后的G1也是使用的复制算法,复制算法的优点和缺点相信大家如果理解之后变会知道,复制算法可以解决内存碎片,因为他是将一个原有区域的对象复制到新的区域并重新排布,而原有区域的对象将不存在,后续使用新区域的对象,就这样依此类推,每次的垃圾收集的时候存活下来的对象和之前存活的对象都会一并放到新的区域中并且会重新排布布局,有效的解决内存碎片问题,但是也正是因为每次垃圾回收之后保留对象的时候都使用复制算法,变相的也就是每次都复制,如果区域块本身的容量不大的话,那么复制过程可以会很快,如果要是内存区块较大的情况下,比如计算机系统本身就有几个T甚至几十个T,而堆内存中的容量也是几百个G,那么可以survivor区域的占用内存为几十个G,试想而知,几百M、几个G、几十个G、几个T、几是个T这种之间的差距都是很明显的,所以复制算法虽好,但是要选择合适的场景来进行使用,否则有效的解决内存碎片的同时也带来了大量的耗时占比,严重导致吞吐量下降,延迟性上升。

3.4 分代思想

(老年代、年轻代区分对象)

​​ ​ ​ 描述:假设没有分代收集器,当我们进行垃圾回收的时候并不是针对于年轻代或者老年代,而是针对于整个堆内容进行扫描,而堆内存中肯定会存在一些生命周期较长或者生命周期较短的对象,如果较短的对象则还好,扫描到的话之后会被回收,但是如果生命周期较长的对象被扫描到的话,发现该对象被引用中,则不会被回收,如果偶尔以这种方式扫描,那可能还好,但是每一次的垃圾扫描都会是这样的扫描方式,则在效率上是很慢的,会导致程序很死,所以当有了分代思想之后,生命周期较短的对象(也就意味者存活率较低对象)放在年轻代,而生命周期较长的对象(存活率较高并且引用较多)放在老年代,而年轻代中有专属垃圾回收器,老年代中有专属回收垃圾器,两个回收算法各个不同,年轻代触发垃圾回收频率较高(朝生夕死),老年代触发垃圾回收器较低(老死不如赖活着)。

3.5 可达性分析算法

​ ​​ ​ ​ 描述:可达性分析算法和引用计数算法的概念大差不差的,标记可达对象,可达对象是正常使用或被引用对象,不可达对象代表无任何引用即将被回收的对象,而可达性属性也存储在对象头信息中。

3.6 引用计数算法—扩展

​​ ​ ​ 描述:简单来讲的话引用计数算法是比较快的,每个对象的头部都会包含一个计数属性信息,当该属性值为1的时候则代表该对象被引用,如果是0的话则代表该对象没被引用,但是如果当对象A依赖对象B,对象B依赖A,这种依赖关系的时候就会出现无法清理问题,复杂的依赖无法解决,由于Java垃圾收集器算法使用的不是引用计数算法,而是可达性分析算法,所以针对于引用计数算法的概念我了解也不算太多,不过好像听说python使用的是引用计数算法,并且针对于复杂依赖场景做出了有效的解决方案。

3.7 内存碎片—扩展

​​ ​ ​ 描述:内存碎片则就是内存中的残留垃圾,可以一小块内存碎片不足以为难,但是N块内存碎片的恐怖是无法预知的,最直接的场景则就是OOM,当然小块的内存碎片并不会导致OOM,但是OOM触发的原因中一定包含一些内存碎片的原因,OOM(OutMemoryError内存溢出)的触发场景最直接的就是堆内存爆满,堆内存不够用。
在这里插入图片描述

3.8 Reset描述

​ Reset核心解决:避免扫描整个堆内存

​​ ​ ​ 描述:Reset(Remember set),夸张一点,它算是属于垃圾回收核心的一点了,因为它的存在,避免了重复性扫描的问题,为什么呢?咯耶~往下看,试想场景1:如果对象A依赖对象B并且它们在同一个场景下,那么后续该对象失效后,标记阶段的时候扫描也会方便,毕竟在同一个区域中,无论是扫描还是清理,都是很方便,但试想场景2:如果对象A在老年代中,而对象B在年轻代中,那请问这样的场景怎么办?难道我扫描年轻代的时候也要把对象A所在的老年代一并扫描吗?这是不行的,我们使用分代思想的前提就是解决整个堆扫描,如果要是因为场景2导致的问题又重现了整个堆扫描的话,那岂不是绝望+失望吗?所以为了避免这样的问题,Reset就出现了,记忆集,什么是记忆集呢?当对象A调用对象对象B的时候,由于跨代调用,所以在被调用者所在区域中记录该调用者的信息,这样会避免扫描整个堆内存,不过这样的前提也是有付出的,每次扫描的时候都会实时更新reset中的信息,会保证数据的实时性,不过扫描是并发扫描并不会影响用户线程的运行。 实际上看到这里,大家肯定会有疑问,就是说,那我即使扫描整个堆也没关系啊,反正是i并发扫描也不会影响用户线程,如果要是这样理解的话那就错了,因为如果要是堆扫描的话,试想场景3:比如年轻代中的对象A依赖老年代中对象B,而年轻代中的对象C也依赖于老年代的对象B,如果出现这种场景,没有记忆集的话就要进行两次扫描堆,而如果存在记忆集的话则在第一次扫描对象A和对象B的时候就记录了对象B的消费信息,所以等到对象C在扫描的时候,直接查看记忆集即可。
在这里插入图片描述

4. G1垃圾回收的详细过程

4.1 垃圾回收器工作流程

​​ ​ ​ 描述:当程序启动时候,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动年轻代的垃圾回收过程,扫描、标记、将不再引用的对象进行回收,而还在Eden区的对象会别复制到survivor1区域中,依此类推执行中,但不同点在于survivor1和其他的survior区不同的切换,当Eden复制存活的对象时,会连同之前已存在的对象一同复制到新的survivor区域中,等到下一次的时候,Eden又会携带存活对象以及之前存活对象一同复制到另一个survivor区,依次类推,当survivor区域中的对象年龄达到阈值,也就是15的时候,会将它放置到老年代中,对象年龄属性存储在对象头信息中,如果survivor区域爆满时,Eden的存活对象要存储时,发现survivor区域无占用区域时,会直接存储到老年代中,或者要存储的对象属于大对象的时,也会直接到老年代中,当年轻代垃圾回收扫描年轻代的时,发现该区域有依赖对象是其他区域的对象,如果要是同一个区域,则免除扫描reset,如果要是Eden区域对象依赖老年代区域中的对象时,就会通过reset扫描来查看,如果引用在的话则标记可达对象,如果引用不存在则等待被回收,等到垃圾回收之后,该reset会更新(更新实时的信息),更新完成之后,其他的流程接着进行,由于采用的是复制算法,所以不会出现内存碎片,要注意的是,当活的对象存储到survivor区域时,这个survivor的角色会变成from,而其他空闲的survivor的区域角色为to,等到下次存储Eden区域中存活对象+from区域存活对象时候会复制到to角色的survivor区域,然后之前的from角色的survivor区域变换成to角色,之前的to角色的survivor区域变成from角色,依此类推,等待对象年龄达到一定阈值的时候会存储到老年代中,特别注意:survivor爆满的话是不会触发GC的,gc在处理Eden的时候会顺带上survivor的,但是survivor本身并不会触发gc,当越来越多的对象(存活的对象或大对象)存储到老年代区域的时候,为了避免堆内存被耗尽,这时候老年代的gc就要触发了,它要扫描老年代中的对象信息,毕竟对象到达老年代是因为生命周期较长,但并不是无终止的生命周期,老年代会触发混合回收(年轻代+老年代),混合回收的开启式随着老年代的内存占比而触发的,默认的话是65%的时候就会触发(老年代垃圾占比65%,参数可以设置,不能太高(如果垃圾的生产速度高于垃圾的回收速度就会触发Full gc),也不能太低(毕竟垃圾回收也是需要时间做出代价的)),混合回收阶段,当老年区的垃圾占比达到一定默认值之后就会进行混合回收,年轻代和部分老年代进行回收,并且尽可能的在指定的时间范围之内回收价值高的region区域,g1有个优先列表,gc会根据该优先列表进行回收价值较高的区域,并且尽可能的在指定时间内完成回收,而优先列表的计算规则是和占比挂钩的,大于或等于65%的靠前,低于65%的以此进行排序,而如果小于10%的区域的话不会触发gc,因为回收也是要时间成本的,10%以下的话gc可能认为没必要把,不过65%以及10%都是可以通过参数进行设置的,不过对于这种参数的设置,还是使用默认的较好。说到这里我们大概已经知道了g1的回收过程,但是扫描标记过程我们却一窍不通,所以下面要讲一下扫描标记过程,标记采用的是并发标记,而首先通过初始标记gcroots的直接对象(期间触发stw,但耗时短,毕竟直接对象和间接对象还是有区别的),等到初始标记记完成之后,会通过并发标记来标记gcroots的间接对象(不会触发stw),而等待标记完成之后,会进入重新标记阶段(stw触发,目的是为了修改并发标记过程中的一些残留对象,因为并发标记是和用户线程同时后进行的,所以再并发标记过程中可能会漏掉用户线程执行过程中的一些新对象),当重新标记阶段完成之后,gc会对各个区域进行垃圾占比计算(stw触发,和region的占比挂钩),计算出权限较高的,记录再优先列表中,后续于混合回收使用,而g1的备选方案则就是full gc(stw触发,处理整个堆),说明:比如说垃圾生产的速度已经超过G1的回收速度了,那无奈之下只能调用备选方案,也就是触发Full gc进行一个全堆的垃圾回收,如果Full gc都不起作用的话,那JVM就会抛出OOM(OutOfMomeryError)内存溢出错误,针对于这点我们也可以通过 垃圾回收日志来查看,如果程序报错oom的话,那么它最后一次调用的垃圾回收一定是Full gc。

​​ ​ ​ G1的回收机制属于分代回收,官方原话则是:“在延迟性可控的基础之上,将吞吐量提高”,协同并发标记和并行回收,越是在多核内存空间大的场景下,G1的优点越是发挥的淋漓尽致。
在这里插入图片描述

5.官方文档

   G1官方文档

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值