java基础巩固-宇宙第一AiYWM:为了维持生计,JVM_Part5~(垃圾收集器&垃圾收集算法etc..)整起

116 阅读20分钟

详细的图又需要的,评论区留言,后面发给你 自己整理的笔记框图

上回正说着,保洁队长和他的几个组因为太饿,给跑路了,给打手都快气坏了,还没打扫好就撒腿跑了。 之前已经提到过下面几个点

  • 打手有的地可以自己打扫(比如线程私有的本地方法栈等,就不需要垃圾回收器帮咱们来处理垃圾),不需要别人帮咱们来打扫点点有惊喜
  • 打手们共享的,比如拳台呀、小胡和敏小言的民政&局故事(指的是堆和方法区)需要专人来帮忙打扫。

现在还有几个问题就是:

  • 保洁组长手底下那几个组到底是谁打扫哪里呀?
  • 用啥工具打扫呀?

相信不仅我作为读者有这个疑问,打手估计都快愁死了!!! 快到大年三十了,打手和保洁组长都急了,所以保洁组长也很迅速的带着人手来了。而且保洁队还托着几个大块头来了。 打手急了:哎哎哎,啊,你们轻点,我还有俩客人,小胡和敏小言还坐那准备看我的比赛呢,况且你们这大块头别给我地震坏了。 保洁组张连忙说,没事,我们这几个都很安全而且效率很高,别怕。 打手:你们这拉的都是啥呀。 保洁组长:忘了说了,我们打扫卫生并不是按老一套那种每个组带着不同数量的扫帚和拖把等工具去到自己不同的地盘不停扫扫拖拖。我们这个大块头总共分七个种类,他们之间一些也可以联合使用(连线的代表两个收集器可以搭配使用)。 在这里插入图片描述 在这里插入图片描述

  • 收集器没有谁比谁好,我们选择的是对具体应用最合适的收集器

在这里插入图片描述

打手:你这啥呀,我都看不清 保洁:我这只是个大概的展示图,下面是具体的细节,别着急呀,且听我慢慢道来。 打手:好好好,看你一会咋介绍你这几个大宝贝 打手,对了,就算你不用普通的工具,你地盘也还是得划分一下吧 保洁组长:当然喽,工具和地盘缺一不可。我们地盘是这样划分的,算法,不和你叨叨叨了,给你看看明明白白的图。 在这里插入图片描述

打手,现在就可以给你介绍一下我们带来的大块头了。 我们大块头按照存在位置是这样分类的 在这里插入图片描述

  • 位于新生代中的,(我们这批大块头相对来说是比较新的,新生代里面嘛),下面这几个
    • Serial:
      • 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
        • 单线程不是说他只会使用个CPU或者一条收集线程去完成垃圾回收工作,而是**在进行垃圾收集时必须暂停其他所有的工作线程直到它收集结束(虚拟机在后台自动发起和完成,用户不可见的情况下把用户正常工作的线程全部停掉)**。
      • 实际上到现在,虽然缺点明显,但是**Serial依然是虚拟机运行在Client模式下的默认新生代收集器**,
      • 它的最大特点是
        • 简单高效(单个CPU环境时没有线程交互的开销可以专心做垃圾收集,自然可以获得最高的单线程收集效率)
        • 在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),(相当于咱们计算机每运行一个小时就得暂停响应五分钟,忙的时候肯定不方便),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,**是client级别的默认GC方式** 在这里插入图片描述 在这里插入图片描述
    • ParNew:Serial收集器的多线程版本,也需要stop the world,复制算法
      • 使用多线程进行垃圾收集
      • 是许多运行在Server模式下的虚拟机首选的新生代收集器(一个很重要的原因是除了Serial收集器外目前只有ParNew能与CMS收集器配合使用)
      • 其余的行为与Serial收集器完全一样(在实现上这两种收集器也共用了很多代码),比如:
        • Serial收集器可用的所有控制参数(比如:-XX:SurvivorRatio、-XX:PretenureThreshord、-XX:HandlePromotionFailure)
        • 收集算法
        • Stop The World
        • 对象分配规则
        • 回收策略等 在这里插入图片描述
    • Parallel Scavenge:吞吐量优先的收集器
      • 新生代收集器,使用复制算法的收集器并发的多线程收集器,目标是达到一个可控的吞吐量(吞吐量指的是CPU用于运行用户代码的时间和CPU总消耗时间的比值(CPU总消耗时间=运行用户代码时间+垃圾收集时间),高吞吐量可以高效利用CPU时间),和ParNew的最大区别是GC自动调节策略
        • 虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量,Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
          • 控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数。
          • 直接设置吞吐量大小的-XX:GCTimeRatio参数 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

老规矩,买一赠一 在这里插入图片描述

  • 位于老年代中(相对来说年代久远一点的机器)
    • CMS:是一种以**获得最短回收停顿时间为目标的收集器**,基于标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片
      • CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿
      • CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
        • 初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象(标记一下GC Roots能直接关联到的对象),这个过程会 Stop The World,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快
        • 并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 Stop The World。
        • 重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。这个过程会Stop The World。还有一个原因就是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
        • 并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段CMS收集器的内存回收过程也是可以与用户线程同时并发进行的
      • CMS优点:
        • 并发收集
        • 低停顿
      • CMS 的问题:
        • 并发回收导致CPU资源紧张:在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大
        • . 无法清理浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾” 在这里插入图片描述
        • 并发失败(Concurrent Mode Failure):由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了
        • 内存碎片问题:CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况 在这里插入图片描述
          • 为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程**,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。**还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理) 在这里插入图片描述
    • Serial Old:Serial收集器的老年代版本,单线程收集器使用标记整理算法
      • 这个收集器的主要意义就是在于给Client模式下的虚拟机使用。
      • 如果在Server模式下他还有两大用途:
        • 在JDK1.5以及之前版本中与Parallel Scavenge收集器搭配使用
        • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用 在这里插入图片描述
    • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程标记-整理算法 在这里插入图片描述
    • G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率
      • G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的
      • 与其他GC收集器相比G1的特点:
        • G1能充分利用多CPU、多核环境下的硬件优势。 在这里插入图片描述
        • 分代收集:不需要其他收集器配合,可以独立管理整个GC堆,能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以及获取更好的收集效果。
        • 空间整合:G1从整体看是基于标记整理算法实现的收集器,从局部(两个Region之间)看是基于复制算法实现的。G1运作期间不会产生内存空间碎片,收集后可以提供规整的可用内存。
          • G1将这个Java堆分为多个大小相同的独立区域(Region),虽然还保留有新生代和老年代的概念,但是新生代和老年代已经不再是物理隔离得了,他们都是一部分Region(不需要是连续的)的集合 在这里插入图片描述
        • 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。
          • G1收集器之所以能够建立可预测的停顿时间模型是因为G1可以有计划的避免在整个Java堆中进行全区域的垃圾收集。 在这里插入图片描述
      • G1 回收器的运作过程大致可分为四个步骤:
        • 初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
        • 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象
        • 最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。(修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录) 在这里插入图片描述
        • 清理阶段(筛选回收)(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的 在这里插入图片描述 在这里插入图片描述 老规矩,买一赠一 在这里插入图片描述 打手:新生代里面的大块头和老年代里面的大块头是按设备新旧来划分的嘛,我咋感觉不是呢,而且还有一个居中的G1呢 保洁组长:肯定不是啦,我就是为了让你好记,到时候活没干好分不清是谁的锅。 哦对了,说起这个G1,算是我们对里比较优秀的一个了,他的工作过程也比较独特,就在图里面写着呢,你可以瞅瞅。 在这里插入图片描述
  • JVM中一次完整的GC:

在这里插入图片描述 老规矩,赠品增增增不停: 在这里插入图片描述 打手:行啦行啦,贴这么多图,这大块头懂得差不多了。那你们这具体是咋打扫呢,别空有一身漂亮的皮囊。 保洁组长:好好好,还猴急的不行。 保洁组长:我们这根据你这边给的钱的多少分为轻轻描边扫和重重锤地霹雳胖浪扫 打手,啥......还分轻重,你这也没说呀,你这不坑人吗, 保洁组长:先别急嘛,我们这有新手福利,你第一次不会受损的...... 打手:切,信nen个鬼哟 保洁队长:先别急,让我装完再说。这是我们的打扫计划 在这里插入图片描述 Minor GC 和 Full GC 有什么不同呢?

  • Minor GC:只收集新生代的GC。 Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。

  • Minor GC触发条件:当Eden区满时,触发Minor GC。

  • Full GC触发条件:

    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
    • 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
    • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
    • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 在 Java 中,堆被划分成两个不同的区域: 在这里插入图片描述

    • 新生代 ( Young ):新生代默认占总空间的 1/3。新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
      • 新生代的垃圾回收**(又称Minor GC)后只有少量对象存活,所以选用复制算法**,只需要少量的复制成本就可以完成回收
    • 老年代 ( Old ):老年代默认占 2/3。
      • 老年代的垃圾回收**(又称Major GC)通常使用“标记-清理”或“标记-整理”**算法

具体的流程如下:

  • 对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
    • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
    • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
    • 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
    • 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;
    • Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代
  • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代

打手,快继续,你这很明显没说完嘛 在这里插入图片描述 在这里插入图片描述 打手:虽然挺细的,但是我还有不知道,你们这怎样规划我们这里不同类的垃圾呢,分类好再进行回收。 保洁组长:你还真是要把我榨干呀。行行行,给你再补充一点。

  • 对象的内存分配与回收策略:
    • 内存分配: 在这里插入图片描述
    • 回收策略: 在这里插入图片描述 补充图:
      • 3.长期存活的对象进入老年代:结合图看。对象在Survivor区中每熬过一次Minor GC年龄就增加一岁,当对象的的年龄增加到一定程度(默认为15随)就会被晋升到老年代。定义年龄的阈值其实也就是定义对象晋升老年代的年龄阈值
      • 5.空间分配担保原则:类似于贷款,收不回来帐时就从你的担保人卡里扣 在这里插入图片描述 在这里插入图片描述
  • 如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC
  • 在允许担保失败并尝试进行YoungGC后,可能会出现三种情况
    • YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
    • YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代
    • YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了

保洁:给你展示的还行吧 打手:还可以,一般般吧,快扫吧......(辅助着学还行吧,深度还是不足哟) 10分钟后..... 保洁队长:好了,你看看。 打手:还凑合吧,可以了,要不一块去吃过饭,庆一下功。 保洁员:你......掏钱? 打手:嗯.....哎。老胡,带上你家敏敏,一块吃个饭呗 敏小言和小胡夫妇:中中中,go 打手:我给你们说......我听说还有个啥

  • Springboot
  • 和多线程和高并发

的故事呢?讲吗? 我:讲讲讲,继续有图有真相,谝起来,改天再见喽