JVM 六. 垃圾回收相关

一. 基础概述

先了解几个常见面试题:讲一下JVM的GC

  1. 什么是垃圾,为什么要有GC
  2. java中垃圾回收重点区域是哪里
  3. 怎么回收的,回收算法是什么
  4. 哪些不会被回收
  5. 垃圾收集器有哪些,有什么不同
  1. 什么是垃圾: 在程序运行过程中,没有任何指针指向或没有被引用的对象就会被判定为垃圾,在执行垃圾回收是需要被回收掉
  2. 为什么要有GC: 清理内存空间,整理内存碎片,执行某个功能动作,程序运行,需要创建对象,当动作执行完毕,该对象可能不在需要,如果不及时清理掉会一直占用内存空间,当存满时会造成内存溢出问题,在垃圾回收期间也会对内存碎片进行整理,将占用的堆内存进行统一整理到一个位置,方便JVM将剩余空间分配给其它对象使用
  3. java是自动垃圾回收,我们为什么还需要了解垃圾回收: 对于java而言垃圾回收像一个黑匣子,自动执行的,但是我们要能定位到内存溢出,内存泄漏的问题,能够根据错误异常日志快速定位和解决问题,并且当垃圾收集系统达到瓶颈时我们要进行相关的监控调优工作
  4. 垃圾回收重点区域是什么: 垃圾回收只存在于堆和方法区中,频繁收集新生代, 较少收集老年代, 基本不会收集元空间永久代
  5. System.gc()或Runtime.getRuntime().gc() 会显示的触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃的对象占用的内存,但是System.gc()并不能保证垃圾收集器马上执行
  6. 被判定为垃圾的对象在执行垃圾回收时会自动调用当前类中的 finalize()方法,如果没有会调用父类的

二. 垃圾回收算法

先了解几个垃圾回收相关的面试题

  1. 常见的垃圾回收算法有哪些:
  2. 不同的垃圾回收算法有什么区别
  3. 垃圾回收算法怎么确定哪些对象可以被回收哪些对象不可以被回收
  4. 不同的垃圾回收算法分别适用于哪些场景为什么
  5. 不同的回收算法对应哪些收集器
  6. 如果让你优化垃圾回收算法有什么思路
  7. 实际工作中采用的那种垃圾回收算法
  1. 垃圾回收在整个过程中可分为两个阶段:1判断垃圾阶段,2垃圾标记和清除阶段,了解垃圾回收算法可以从这两个角度去分析

判断垃圾阶段

  1. 判断垃圾阶段, 判断对象是不是垃圾,当一个对象不再被任何存货的对象继续引用时,该对象就被判定为垃圾,该阶段有两个算法: 引用计数法, 可达性分析法

引用计数算法

  1. 引用计数算法: 创建出的对象中有一个引用计数器,当有一个引用时计数器加1,当引用失效时计数器减1,当计数器值为0,垃圾回收机制会判定该对象为垃圾
  2. 引用计数算法的优缺点:
  1. 优点: 查询一下引用计数为0的就判断为垃圾,实现简单,垃圾对象便于识别,判定效率高,回收没有延迟性,
  2. 缺点:假设有a,b两个对象,两个对象之间相互引用,后续操作中a,b两个对象都设置为null不在使用,原本可以被判定为垃圾进行会收的,由于两个对象之间的相互引用,计数器都为1,造成回收不掉的问题
  3. 引用计数算法的应用: java中由于循环依赖问题,没有采用引用计数算法,但是业界为了提高吞吐量有使用的例如python,手动去解除.或通过弱引用解决循环引用问题

可达性分析算法/根搜索算法 GC Roots

  1. 什么是GC Roots,将方法区,虚拟机栈和本地栈看为各种级别的Gc Roots,如果堆中的对象与任何Roots存在引用,就说明该对象可用,不进行回收,jvm在进行垃圾回收时会根据不同级别的GcRoots向下寻找,如果对象间的引用与Roots引用链断开就进行垃圾回收
    在这里插入图片描述
  2. 哪些可以作为 GC Roots 根: 虚拟机栈, 本地方法栈, 类静成员, 方法区中常量, 所有被synchronized持有的对象, java虚拟机内部的引用(Root 采用栈方式存放遍历和指针,如果一个指针保存了堆内存里的对象,但是自己又不存放在对内存中,就可以将它看成根)
    在这里插入图片描述
  3. 可达性分析算法优点: 相较于引用计数算法,可达性分析算法同样具有实现简单,执行效率高的优点,并且解决了循环引用的问题,只要对象实例与根之间的引用链有断开,即使对象间相互引用,整个引用环都会被回收掉
  4. 可达性分析算法应用场景: 在java, C#中采用该垃圾回收算法
  5. 根搜索算法是导致 Stop The World 原因的其中一个: 判定内存是否可以回收时分析工作必须在一个能保障一致性的快照中进行的,如果不满足,分析结果的准确性就无法保证,即使号称(几乎)不会发生停顿的GMS收集器,在枚举根节点是也必须停顿

垃圾标记和清除阶段

垃圾标记和清除阶中的算法有: 标记清除算法, 复制算法, 标记有压缩算法, 分代收集算法, 增量收集算法, 分区算法等

标记清除算法

  1. 标记清除算法中可以分两个阶段: 标记阶段 与 清除阶段
  1. 标记阶段: 先判断对象是否存活,标记出所有存活对象,例如:有根节点引用的可存活对象标记加1(初始化时为0), 无引用的不可存活对象标记减1
  2. 清除阶段:将没有标记的对象也就是不可存活的清除(注意此处的清除并不是真的清除,而是把需要清除的对象地址保存到一个列表中进行记录,下次有新对象时判断这个列表中记录的对象的空间是否够,如果够再存放)
    在这里插入图片描述
  1. 优点: 由根节点开始解决引用计数法的循环依赖问题,必要时才进行内存回收(内存不足时)
  2. 缺点:
  1. 由根节点开始存在两次全堆遍历: 第一次遍历标记不可回收对象,第二次遍历获取未被标记的可回收对象进行回收效率比较低,
  2. 不连续造成内存空间碎片化,将内存空间割裂成很多小部分,碎片化的最大空间可能小于当前创建的这个大对象,出现存不下的情况
  3. GC时为了保证一致性需要停止整个应用程序Stop The World,用户体验差
  1. 适用场景: 适用于老年代,对象生命周期较长,垃圾回收频率较低的(CMS垃圾收集器使用,其它由于内存碎片化一般不会使用)

复制算法(通常用在新生代)

  1. 先简述一下: 使用两块空间存储对象,每次只使用其中一块,在垃圾回收时,将指定块中的存活对象复制到另一块内存区域中,然后先前使用的这块内存区域中就只剩下不存活的对象了,清除整个区域即可
  2. JVM新生代的复制算法:
  1. 首先在堆内存布局: 堆内存中划分为新生代与老年代区域,新生代又分为新生代中又分为三个部分,eden与s0, s1
  2. 新创建的对象首先会存在在eden伊甸园区,当经历垃圾回收后会将伊甸园区的对方放入Suvivor的From幸存区,当再执行垃圾回收时,会获取From区域中的存活对象,将这些存活对象复制到Suvivor的To区,然后清空From区
  3. 在Survivor通常不会出现存不下的情况下,原因:
  1. 当创建对象过大时会直接将这个对象放入老年代
  2. 当创建的对象被jvm垃圾回收执行15次后判断还是可达对象,会将该对象放入老年代中,老年代的垃圾回收频率比新生代的底(或者当内存已满时触发垃圾回收等),
  3. 在执行垃圾回收时Survivor区中相同年龄的所有对象大小总和大于Survivo空间的一半时这些对象会直接进入老年代,如果老年代判断存不下时会触发FullGC
  1. 举例: 新创建的对象a1与a2存放在eden中,当jvm进行垃圾回收时判断这对象还是可用对象,就将a1与a2对象放入s0中,然后创建a3对象,如果jvm下次执行垃圾回收判断a3也是可用对象,会将a3也存入s0中,如果下次jvm执行垃圾回收时判断a1变为了不可用对象,会将可用的对象a2与a3复制到s1中,然后删除掉存有不可用对象a1, 的s0中所有数据, 此时如果再次创建对象a4放入eden中,jvm执行垃圾回收,判断a4是存活对象,会将a4放入存有数据的s1中,而不是s0,当判断有不可存活对象时将s1中可存存活的复制到s0中,删除s1中所有数据
  1. 优点: 没有标记清除两个阶段,在存活对象不多的情况下性能高,存活对象复制后,清除不存活对象的整个空间解决了标记算法的碎片化问题
  2. 缺点:
  1. 需要两倍的内存空间会造成一部分的资浪费,不过可以根据实际情况,将内存块大小比例做适当调整,
  2. 对于G1这种拆分为大量region的GC,复制而不是移动,意味着GC要维护region之间对象引用关系,内存占用,时间开销都不小
  3. 如果存活对象数量比较大时,来回复制,会影响性能

标记压缩算法

  1. 与标记清除类似,但是多了一个压缩步骤
  1. 第一标记阶段, 从根节点开始标记所有被引用的对象
  2. 第二压缩阶段, 将所有存活对象压缩到内存的一端,按顺序排放
  3. 第三清除阶段, 清除存活对象边界以外所有空间
  1. 标记清除与标记压缩两个算法可以看为是:非移动式回收与移动式回收,通过移动将存活对象放到同一端,解决了标记清除时的内存碎片问题,也解决了复制算法时的两块内存资源浪费的情况,但是移动+回收是一项优缺点并存的
  2. 缺点:
  1. 执行效率方面低于复制算法,不仅要标记所有存活对象,还要整理所有存活对象的引用地址
  2. 对于老年代,每次都会有大量存活对象,如果采用标记压缩算法,移动这些存活对象消耗较大
  3. 移动对象的同时,如果对象被其它对象引用,需要同步更新引用该对象的持有的地址值
  4. 同时也存在GC时为了保证一致性需要停止整个应用程序Stop The World,用户体验差

分代算法

  1. 先了解一下: 标记清除, 标记压缩, 复制算法多方面对比情况
  1. 复制算法最快,担心需要两块内存空间浪费
  2. 标记压缩算法,比复制多了一个标记阶段,比标记清除算法多了一个整理阶段,但是解决了标记清除的内存碎片化问题
    在这里插入图片描述
  1. java中基于不同对象生命周期不同,不同生命周期的对象采用不同的手机方式,提出分代,分为新生代跟老年代,根据各个代的特点使用不同的回收算法,进而达到性能的提高
  1. 新生代特点: 区域相对老年代较小,对象生命周期短,存活率低,回收频繁,通过两个Suvivor使用复制算法实现
  2. 老年代特点: 区域较大, 对象生命周期长, 回收频率没有新生代高,一般是由标记清除或标记清除+标记整理混合使用实现
  1. 以HotSpot中的CMS收集器为例,新生代采用复制算法,老年代采用标记清除算法,当碎片导致Concurrent Mode Failure时,采用以标记压缩作为回收算法的Serial Old执行FullGC以达到对老年代的内存整理进行补偿

增量收集算法

  1. 上面解释的算法都存在一个问题,为保证一致性,在垃圾回收过程中会出现端在的Stop the World 状态,应用程序中所有线程都处于挂起状态,等垃圾回收完毕才会正常,假设垃圾回收的实际过长将严重影响用户体验,为了解决垃圾回收过程中出现Stop the World 挂起所有线程的状态,诞生了增量收集算法
  2. 基本思想: 垃圾回收线程和应用程序线程,两个线程交替执行,垃圾线程每次只收集一小块区域的内存,接着切换回应用线程一次返回,直到垃圾回收完毕,增量收集算法仍然使用的标记清除,复制算法,通过对线程冲突的妥善处理,允许垃圾收集线程分阶段的方式完成标记,清理复制工作(简单解释就是多线程,频繁切换,给用户一种没有停顿的的感觉),会减少延迟问题,但是会降低吞吐量垃圾收集线程为了提高速度一次只收集一小部区域

三. 什么是STW

安全点与安全区域

  1. JVM在定内存是否可以执行回收时,必须在一个能保障一致性的快照中进行的,如果不满足,分析垃圾的结果的准确性就无法保证,所以通过挂起所有用户线程来保证,这个挂起是JVM在后台自动发起的,也就是我们说的Stop the World
  2. 进而引出什么时候挂起用户线程: 是在一个特定的位置挂起用户线程,这个特定的位置被称为安全点SefePoint,
  3. 怎么取确定安全点,如果太少可能会导致一直寻找安全点GC时间等待过长,如果太多又可能出现运行时的性能问题: 通常以"是否具有让程序长时间执行的特征"为标准进行选择,例如选择一些执行时间较长的指令作为安全点如: 法调用,循环跳转,异常跳转等等
  4. GC发生时检查所有线程都跑到最近的安全点停顿下来的,有两种方式: 抢先式中断(现已不采用), 主动式中断
  1. 抢先式中断: 首先中断线程,如果线程还未到达安全点,再恢复未到达安全点的线程
  2. 主动式中断: 设置一个中断标志,各个线程运行到安全点后主动轮询这个标志,当中断标志位true时才挂起线程
  1. STW在这里插入图片描述
  2. 进而提出了"串行回收, 并发回收,并行回收"的三种回收方式,那么: 什么是串行回收,并发回收,并行回收
  1. 串行回收: 是指同一时间内只允许一个垃圾收集线程执行(挂起用户线程)
  2. 并行回收: 是指多个垃圾收集线程同时执行(挂起用户线程)
  3. 并发回收: 是指用户线程与垃圾收集线程同时或交替执行,
  1. 解决STW考虑增量回收算法,也就是并发,用户线程垃圾回收线程交替执行
  2. 对应串行,并行,并发的垃圾收集器
  1. 串行: 挂起用户线程,同一时间内只允许一个垃圾收集线程执行:
  2. 并行: 挂起用户线程,多条垃圾收集线程并行工作: ParNew, Parallel Scavenge, Parallel Old
  3. 并发: 用户线程与垃圾收集线程同时或交替执行的: CMS, G1

四. 垃圾回收器

  1. 在工作模式上区分垃圾收集器: 并发式, 独占式,
  1. 并发式垃圾收集器: 用户线程与垃圾收集线程可以同时执行,应用程序线程与垃圾收集器之间交替工作,可以尽可能的减少应用程序的停顿时间,
  2. 独占式垃圾收集器: 挂起用户线程只运行一个或多个垃圾收集线程执行的串行,并行收集,该收集器一旦运行就会挂起用户线程,知道垃圾收集结束(Stop the World)
  1. 在是否压缩层面可以区分垃圾收集器为:压缩式与非压缩式, 压缩式垃圾收集器在收集完毕后,对存活对象进行压缩整理,清除回收后的碎片,非压缩式垃圾收集器: 不会进行整理
  2. 如何评估一个垃圾收集器的好坏,垃圾收集器的评估指标:
  1. 吞吐量: 程序线程执行时间/(程序线程执行时间+垃圾收集时间)
  2. 垃圾收集开销: 吞吐量的补数, 也就是垃圾收集所占时间与总时间的比例(程序线程执行时间+垃圾收集时间)
  3. 暂停时间: 执行垃圾收集时,程序的工作线程挂起的时间
  4. 收集频率: 相对于应用程序的执行,收集操作的发生频率
  5. 内存占用: java堆所占用内存的大小
  6. 速度: 一个对象从诞生到被回收经历的时间
  1. 由上面评估垃圾收集器好坏其中两个比较重要的: 总结一句话就是: 在最大吞吐量的情况下降低停顿时间
  1. 吞吐量优先: 单位时间内,吞吐量越大越好
  2. 响应时间优先(暂停时间越少): 尽可能让单次STW的时间最短
  1. 常见的垃圾收集器有哪些: 7种,根据串行收集器, 并行收集器, 并发收集器分为
  1. 串行收集器: Serial(C蕊偶), SerialOld
  2. 并行收集器: ParNew, ParallelScavenge(排额来袄 私嘎文之), ParallelOld
  3. 并发收集: CMS, G1
  1. 7种垃圾收集器搭配使用关系(不管实线虚线,只要有链连接就是搭配使用的,为什么同样是老年代的CMSGC与SerialOldGc有连线,因为CMS采用标记清除算法,会出现内存碎片,如果内存碎片导致Concurrent Mode Failure时,会通过SerialOld执行FullGC达到整理内存效果)
    在这里插入图片描述
  2. 如何查看默认GC: “-XX:+PrintCommandLineFlags” 打印命令行参数,设置该参数后打印的信息中包含垃圾收集器
  3. 历史简述
  1. Serial: JDK1.3使用的串行方式的垃圾收集器,
  2. ParNew: 是对应Serial的多线程版本的并行收集器
  3. Parallel: JDK1.4发布,在JDK1.6后成为HotSpot的默认垃圾收集器
  4. G1: JDK7时出现G1,在JDK9时,G1变为默认垃圾收集器,代替GMS, 并在JDK10时对G1进行了优化实现并行性来改善最坏情况下延时问题,JDK12增强G1, 自动返回未用堆内存给操作系统
  5. Epsilon 与 ZGC: JDK11引入该收集器,被称为"No-Op无操作"收集器,同时引入ZGC:可伸缩低延迟垃圾收集器(未来发展方向)
  6. ShenandoahGC: JDK12引入该低停顿时间GC
  7. JDK14删除CMS垃圾收集器,扩展ZGC
  1. 不同垃圾收集器的应用场景
    在这里插入图片描述

Serial 与 SerialOld 串行回收

  1. Java1.3之前HotSport虚拟机新生代收集器的唯一选择
  2. Serial 串行收集器, 针对新生代使用,采用复制算法,会产生Stop the World,在HotSpot虚拟机Client模式下的默认垃圾收集器
  3. SerialOld 串行收集器, 针对老年代,采用标记压缩算法,会产生Stop the World,在HotSpot虚拟机Client模式下的默认垃圾收集器,
  4. SerialOld有个注意点: 在HotSpot虚拟机为Server模式时,老年代使用CMS收集器,CMS收集器采用标记清除算法,会产生内存碎片,如果内存碎片导致Concurrent Mode Failure时,会通过SerialOld执行FullGC达到整理内存效果
  5. 优点: 对于限定单个CPU的环境来说,简单高效,没有线程交互的开销,在用户的桌面场景应用中,可用内存一般不大,较短时间内完成垃圾收集,只要不频繁发生,串行垃圾收集器是可以接收的,对于交互性较强的应用,是不可以接受的
  6. 设置HotSpot使用Serial为垃圾收集器: “-XX:+UseSerialGC”,此时对应的老年代使用SerialOld

ParNew 并行回收

  1. ParNew对应新生代垃圾收集器简 : Serial是单线程串行收集器,ParNew是Serial多线程并行版,采用复制算法,会挂起用户线程Stop the World,是JVM运行在Server模式下的默认垃圾收集器
  2. 在多核CPU情况下采用ParNew充分利用CPU,速度吞吐量都是高于Serial的,但是单核CPU下性能不如Serial,因为Serial只有一个线程不存在线程切换
  3. 可以使用"-XX:+UseParNewGC"指定使用ParNew 垃圾收集器,通过"-XX:ParallelGCThreads=执行线程数,默认cup相同"
  4. 当设置使用ParNew 为新生代垃圾收集器时,此时对应老年代使用SerialOld作为收集器
  5. **ParNew 可以与CMS垃圾收集器进行配合使用,**当指定老年代使用CMS为垃圾收集器时,此时新生代会设置为ParNew

ParallelGC 与 Parallel Old 吞吐量优先 (JDK8默认垃圾收集器)

  1. ParallelGC 同样采用复制算法,适用于新生代的垃圾收集器,问已经有了ParNew为什么还要出现ParallelGC两个垃圾收集器有什么不同
  2. ParallelGC收集器与ParNew区别:提供了通过自适应调节策略可控的吞吐量设计,可以更好的利用CPU,适用于后台运算较多,交互相对较少的任务场景
  1. ParallelGC垃圾收集器时的自适应条件策略也就解释了堆中伊甸园,s0,s1区占比问题,默认是8:1:1,不设置任何参数的情况下实际查看到的却不一定,就是这个自适应的原因
  2. “-XX:+UseParallelGC” : 设置新生代使用Parallel收集器
  3. “-XX:+UseParallelOldGC”: 设置老年代使用ParallelOld收集器
  4. JDK8时上面两个指令时默认开启的,上面两个命令是互相激活的,
  5. “-XX:ParallelGCThreads”:设置新生代执行垃圾回收时并行收集的线程数,默认情况下当cpu数量小于8时,该值等于CPU数,当CPU大于8时,改制等于3+(5*cup个数/8)
  6. “-XX:MaxGCPauseMillis” 设置垃圾收集器最大停顿时间既STW时间,单位毫秒,为了尽可能停顿时间控制在该值以内,JVM会调整对内存大小,等一些参数,对于用户来说停顿时间越小越好,对于服务端,更注重并发整体的吞吐量,通常情况下使用Parallel自适应,进行控制
  7. “-XX:GCTimeRatio” 垃圾收集时间占总时间的比例,用于衡量吞吐量大小,默认99,也就是垃圾收集时间不超过1%,与前一个"-XX:MaxGCPauseMillis"参数有一定矛盾性,暂停时间长,Radio参数就越容易超过设定的比例
  8. “-XX:+UseAdaptiveSizePolicy” 设置Parrallel Scavenge收集器具有自适应调解策略,该模式下年轻代伊甸园,s0,s1的比例,与晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量,停顿时间之间找到一个平衡点,在手动调优比较困难的情况下可以直接使用该模式
  1. 当新生代设置垃圾收集器为ParallelGC时,对应的老年代会使用ParallelOld,使用标记压缩算法,并行回收,也会产生STW

CMS低延迟

  1. 先了解CMS几个相关问题(java1.5HotSpot推出的一块收集器)
  1. CMS特点: 优点:并发收集,低延迟(用户线程短暂的两次挂起,其它时间都与垃圾回收线程交替执行)
  2. CMS进行垃圾回收时执行流程
  3. CMS进行垃圾回收时有哪几个过程,停顿几次,会不会产生内存碎片,老年代内存碎片会有什么问题
  4. G1和CMS有什么区别
  5. 吞吐量优先,和响应优先的垃圾收集器选择
  6. CMS使用标记清除算法,既然标记清除会参数内存碎片,为什么不使用标记压缩: 因为标记压缩,在内存清理完毕后会移动存活对象位置,如果移动需要修改引用持有的地址值,如果修改防止出现问题就需要STW挂起用户线程,会出现性能下降
  7. CMS那种情况下会触发FullGC, 一个是内存碎片导致对象存不下时,一个是CMS垃圾回收并发标记阶段工作线程不会挂起产生新垃圾,导致对象存不下时,都会触发SerialOld串行收集的FullGC达到内存整理效果,
  1. CMS 采用标记清除算法,针对老年代,用户线程,垃圾回收线程同时工作的并发垃圾收集器,会尽可能的缩短垃圾收集时用户线程的挂起时间STW,常用于B/S架构服务端,只是减少挂起用户线程时间,并不是没有
  2. 需要注意点: CMS作为针对老年代的垃圾收集器,不能与ParallelScavenge新生代垃圾收集器配合使用,在老年代使用CMS时,对应新生代使用ParNew,并且由于CMS采用标记清除算法,会产生内存碎片,当内存碎片导致Concurrent Mode Failure时,搭配采用以标记压缩作为回收算法的Serial Old执行FullGC以达到对老年代的内存整理进行补偿
  3. CMS垃圾回收流程
  1. 初始标记阶段: 该阶段出现第一次STW挂起,标记与GCRoots直接关联的对象,该阶段完成后,恢复STW挂起的线程
  2. 并发标记阶段: 最耗时阶段,由GCRoots开始遍历整个堆中的对象,查看对象是否有被引用,该阶段程序工作线程会不会被挂起一个并发的过程
  3. 重新标记阶段: 由于在并发标记阶段工作线程垃圾回收线程交叉运行,为了防止并发标记期间,因程序工作线程继续执行而导致标记产生变动出现数据不一致问题,重新标记进行修正(例如并发阶段:程序线程将不可达对象修改为了可达对象,将可达对象变为不可达),该阶段会出现第二次STW挂起
  4. 并发清除阶段: 清除被标记阶段判断为死亡的对象,释放内存空间,由于不需要移动存活对象整理内存,此时的程序工作线程也是不需要挂起的
  1. CMS优点:并发收集,低延迟(用户线程短暂的两次挂起,其它时间都与垃圾回收线程交替执行)
  2. CMS缺点:
  1. 使用标记清除算法会产生内存碎片,实际通过搭配一个SerialOld垃圾回收器执行FullGC补偿达到整理内存效果
  2. CMS对CPU资源敏感,在并发阶段,虽然不会挂程序工作线程,但是可能出现占用线程的问题导致程序的吞吐量降低
  3. CMS收集器无法处理浮动垃圾,什么意思: 在并发标记阶段工作线程与垃圾回收线程共存,如果此时的出现新的垃圾,CMS无法对这些新垃圾进行标记,导致新垃圾没有被及时回收
  1. 参数设置
  1. “-XX:+UseConcMarkSweepGC” 手动指定使用CMS收集器,开启后会自动使用ParNew, SerialOld组合
  2. “-XX:CMSlnitiatingOccupanyFraction” 设置堆内存使用率阈值,一旦达到该阈值,就执行垃圾回收,JDK5以前默认68表示老年代使用率达68%,CMS会进行异常垃圾回收,JDK6及以上版本为92%, 如果内存增长慢可以设置一个稍大的值,降低CMS垃圾回收触发频率,减少老年代回收次数,如果内存使用率增长快避免触发老年代SerialOld串行收集
  3. “-XX:+UseCMSCompactAtFullCollection” 用于指定执行完FullGC后对内存空间进行压缩整理,避免内存碎片的产生
  4. “-XX:CMSFullGCsBeforeCompaction” 设置在执行多少次FullGC后对内存空间进行压缩整理
  1. JDK9将CMS标记为disable, JDK14删除

G1区域化分代

  1. G1 面向服务端应用的垃圾收集器,主要针对多核CPU及大容量内存的机器,兼顾吞吐量与停顿时间的
  2. JDK9默认的垃圾收集器,取代了CMS,以及Parallel+ParallelOld, 可以同时兼顾新生代与老生代的"全功能垃圾收集器"
  3. 在G1中会将堆内存分割为很多不相关的区域称为Region(物理上不连续的),使用不同的Region来表示伊甸园区,s0, s1,老年代,G1收集器跟踪各个Region,会维护一个优先列表,根据允许的收集时间在回收时优先回收垃圾最大的区域,所以G1是GarbageFirst又被称为G1
  4. 在Region分区中新增了一个 H 区的概念,如果一个对象的大小超过了一个 Region 的 50%,那么该对象就会被直接存放进 H 区。如果一个 Region 无法存放下对象,那么就会采用连续的多个 Region 来存放该超大对象
    在这里插入图片描述
  5. G1收集器的其它特点:
  1. 支持并行: 在回收期间,可以多个GC线程同时工作,有效利用多CPU计算能力,此时用户线程STW
  2. 支持并发: 在垃圾收集时部分阶段支持工作线程垃圾收集线程共存
  3. 分代收集: 依然属于分代收集,会区分老年代与新生代,与其它回收器不同的是同时兼顾新生代与老年代,将堆划分为若干个Region,
  1. G1针对新生代回收流程: 首先STW停止工作线程,G1创建回收集(CollectionSet),回收集是指需要被回收的内存集合,包括了伊甸园,s0,s1所有的内存分段
  2. G1进行垃圾回收时的执行流程:
  1. 初始标记阶段: 仅仅只是标记出 GC Roots 直接关联的对象(此时当前 Region 中的记忆集也会被当做是 GC Roots),并且还会修改 TAMS 指针,让下一阶段用户线程并发执行时,能够正确的在可用的 Region 中分配新对象。这一步会造成 STW,但是由于只标记和 GC Roots 直接相连的对象,所以暂停时间很短,具体暂停多长时间,和 GC Roots 的数量有关。另外由于该阶段是借用进行 Minor GC 是同步完成的,因此不会额外造成停顿。
  2. 并发标记阶段: 从上一步标记出的对象出发,遍历整个对象图,这一步耗时较长,但是由于是和用户线程并发执行,因此不会造成 STW。
  3. 最终标记阶段: 由于在并发标记阶段,垃圾回收线程和用户线程并发执行,因此在这一过程中,可能会由于用户线程改变了对象的引用关系,造成对象”消失“,因此还需要重新处理 SATB(原始快照)记录下在并发阶段有引用关系改动的对象,这一过程就是在最终标记阶段完成的,会造成 STW,否则如果用户线程还一直进行,就会不停地造成对象引用关系的改变,我们就得不停的处理 SATB 记录。虽然会造成 STW,但毕竟 SATB 记录的引用改变的对象不会特别多,因此耗时比并发标记阶段的耗时会少很多。在这一步中,如果发现当前 Region 中的所有对象都是垃圾对象,那么就会立即对当前 Region 进行回收。
  4. 筛选回收阶段: 负责更新 Region 的统计数据,根据每个 Region 的回收价值和成本进行排序,然后根据用户期望停顿的时间内来指定回收计划,可以选择多个 Region 构成回收集,然后采用复制算法,将 Region 中存活的对象复制到空闲的 Region 中,从而回收 Region。
  1. 只有并发标记阶段不会造成 STW,其他阶段都会产生 STW
  2. 使用G1垃圾回收器的操作步骤
  1. “-XX:+UseG1GC”: 手动指定使用G1收集器
  2. “-XX:+G1HeapRegionSize”: 设置每个Region大小,值是2的幂, 范围是1-38M之间,通常情况下,G1 会将堆内存划分为 2048 个 Region,如果我们指定堆内存的大小为 4G ,那么每个 Region 的大小为 2MB,默认是堆内存的1/2000
  3. “-XX:MaxGCPauseMillis”: 设置期望达到的最大GC停顿时间指标,默认是200ms
  4. “-XX:ParallelGCThread”: 设置STW时GC线程数的值,最大设置为8
  5. “-XX:ConGCThreads”: 设置并发标记的线程数,通常为ParallelGCThread的1/4左右
  6. “-XX:InitiatingHeapOccupancyPercent”: 设置触发并发GC周期的java对占用率阈值,超过该值就触发GC默认45

G1 垃圾回收器的运行细节

  1. G1 垃圾回收器既能回收新生代,又能回收老年代,那么究竟在什么情况下会触发新生代 GC,什么情况下触发老年代 GC 呢?
  2. 什么时候触发新生代 GC
  1. 在 G1 中,Eden、Survivor、老年代的大小是在动态变化的。在初始时,新生代占整个堆内存的 5%,可以通过参"G1NewSizePercent"设置,默认值为 5。(但是新生代依旧可以被分为 Eden 区和 Survivor 区,参数 SurvivorRatio 依旧表示 Eden/Survivor 的比值)
  2. 随着系统的运行,Eden 区的对象越来越多,当达到 Eden 区的最大大小时,就会触发 Minor GC。新生代的最大大小默认为整个堆内存的 60%,可以通过参数"G1MaxNewSizePercent"控制,默认值为 60。
  3. G1 垃圾回收器在进行新生代的垃圾回收时,会采用复制算法来回收垃圾,不用考虑并发的场景,全程都是 STW,它会根据设置的停顿时间,尽可能的最大效率的回收新生代区域。
  1. 什么时候进入到老年代: 新生代的对象要进入老年代,需要达到以下两个条件中的其中之一即可。
  1. 多次躲过新生代的回收, 对象年龄达到「MaxTenuringThreshold」,该参数默认值为 15。 在每次 Minor GC 时,新生代的对象如果存活,会被移动到 Survivor 区中,同时会将对象的年龄加 1,当对象的年龄达到 MaxTenuringThreshold 后,就被被移到老年代中。
  2. 符合动态年龄判断规则。如果某次 Minor GC 过后,发现 Survivor 区中相同年龄的对象达到了 Survivor 的 50%,那么该年龄及以上的对象,会被直接移动到老年代中。 例如某次 Minor GC 过后,Survivor 区中存在年龄分别为 1、2、3、4 的对象,而年龄为 3 的对象超过了 Survivor 区的 50%,那么年龄大于等于 3 的对象,就会被全部移动到老年代中。
  1. 在 G1 中不存在单独回收老年代的行为,而是当要发生老年代的回收时,同时也会对新生代以及大对象进行回收,因此这个阶段称之为混合回收Mixed GC。
  1. 当老年代对堆内存的占比达到 45%时,就会触发混合回收。可以通过参数InitiatingHeapOccupancyPercent来设置当堆内存达到多少时,触发混合 GC,该参数的默认值为 45。
  2. 当触发混合 GC 时,会依次执行初始标记(在 Minor GC 时完成)、并发标记、最终标记、筛选回收这四个过程。最终会根据设置的最大停顿时间,来计算对哪些 Region 区域进行回收带来的收益最大。
  3. 实际上,在筛选回收阶段,可以分多次回收 Region,具体多少次可以通过参数G1MixedGCCountTarget控制,默认值为 8 次。具体什么意思呢?假如现在有 80 个 Region 需要被回收,因为筛选回收阶段会造成 STW,如果一下子全部回收这 80 个 Region,可能造成的停顿时间较长,因此 JVM 会分 8 次来回收这些 Region,每次先回收 10 个 Region,然后让用户线程执行一会,接着再让 GC 线程回收 10 个 Region,直至回收完这 80 个 Region,这样尽可能的降低了系统的暂停时间。
  1. G1 垃圾回收器的回收思路是:不需要对整个堆进行回收,只需要保证垃圾回收的速度大于内存分配的速度即可。因此在每次进行 Mixed GC 时,虽然我们设置了停顿时间,但是当回收得到的空闲 Region 数量达到了整个堆内存的 5%,那么就会停止回收。可以由参数「G1HeapWaterPercent」控制,默认值为 5%。另外,在混合回收的过程中,由于使用的是复制算法,因此当一个 Region 中存活的对象过多的话,复制这个 Region 所耗费的时间就会较多,因此 G1 提供了一个参数,用来控制当存活对象占当前 Region 的比例超过多少后,就不会对该 Region 进行回收。该参数为「G1MixedGCLiveThresholdPercent」,默认值为 85%。
  2. 触发 Full GC: 在进行混合回收时,使用的是复制算法,如果当发现空闲的 Region 大小无法放得下存活对象的内存大小,那么这个时候使用复制算法就会失败,因此此时系统就不得不暂停应用程序,进行一次 Full GC。进行 Full GC 时采用的是单线程进行标记、清理和整理内存,这个过程是非常漫长的,因此应该尽量避免 Full GC 的触发

G1 的调优

  1. MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标,默认是200ms, 设置该值是有两种情况
  1. 设置的太小,虽然在 GC 回收时停顿时间较短,但是每次回收的 Region 也会较少,如果内存分配速度过快,就需要频繁的进行 GC,当回收速度跟不上内存分配速度时,会造成 Full GC。
  2. 设置得过大,那么虽然每次回收可以获得的空闲 Region 较多,但是系统停顿时间会过长,也不好。因此需要结合系统的实际情况,通过相关的工具,实时查看系统的内存情况,从而决定如何调整该参数。
  1. 我们可以从源头上考虑,触发混合 GC 是因为老年代对象过多,而老年代的对象从哪儿来的?当 Survivor 区中的对象年龄达到阈值或者存活的对象数量太多,导致 Survivor 无法容纳下,最终使对象进入到老年代。如果 MaxGCPauseMillis 设置得过大,会导致很久才进行一次新生代回收,由于新生代的对象积攒过多,存活的对象数量也可能比较多,当 Survivor 无法存放下时,可能触发动态年龄判断条件,从而导致对象直接进入到老年代中,进而导致 Mixed GC。如果 MaxGCPauseMillis 设置得过小,导致新生代回收频繁,存活对象的年龄增长过快,从而进入到老年代,又会造成 Mixed GC。因此想要减少 Mixed GC 发生的次数,其核心也是需要控制 MaxGCPauseMillis 参数的大小。
  2. InitiatingHeapOccupancyPercent: 触发 Mixed GC 的条件,当老年代占用堆内存到达 45%时,原则上应尽量减少 Mixed GC 发生的次数, 因此可以适当地调大该值。不建议使用,尽量使用默认值即可。
  3. 关于 G1 垃圾回收器,它有很多参数可以进行设置,在具体使用过程中,如何进行调优,需要结合实际情况进行设置。这里笔者只是提供一个思路,个人认为「MaxGCPauseMillis」参数是 G1 调优的核心,且能对哪些参数进行调优的前提是:需要明白 G1 垃圾收集器的工作原理以及这些参数对 G1 是如何影响的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值