JVM系列(十五):垃圾回收器

1、GC分类与性能指标

1.1、GC分类

1.1.1按线程分类

  • 串行垃圾回收器:同一时间段内仅允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束。默认应用于Client模式,在并发能力强的CPU上。
  • 并行垃圾回收器:运用多个CPU同时执行垃圾回收,提升应用吞吐量,同样是STW制。

1.1.2、按工作模式分类

  • 并发式垃圾回收器:与应用线程交替工作,减少应用程序STW
  • 独占式垃圾回收器:一旦运行就启动STW,直到垃圾回收完毕

1.1.3、按碎片处理方式分

  • 压缩式垃圾回收器:垃圾回收后对存活对象进行压缩整理,消除碎片(指针碰撞)
  • 非压缩式垃圾回收器:垃圾回收后不压缩处理(空闲列表)

1.1.4、按工作内存区间分

  • 年轻代垃圾回收器
  • 老年代垃圾回收器

1.2、性能指标

  • 吞吐量:运行用户代码时间占总时间比例(总时间=用户代码时间+GC时间)
  • 垃圾收集开销:GC时间占总时间比例
  • 暂停时间:执行垃圾收集时,用户程序STW的时间
  • 收集频率:相对于应用程序的执行,垃圾收集操作发生的频率
  • 内存占用:Java堆区
  • 吞吐量、暂停时间、内存占用三者构成“不可能三角”,不能同时满足
  • 当前计算机,内存已经够了,再大了也不好,在现实中,主要抓住吞吐量、暂停时间进行研究
  • 吞吐量和暂停时间只能二选一
  • 以吞吐量优先,那么要降低内存回收的执行效率
  • 低延迟优先,那么频繁GC,造成线程切换,吞吐量下降

2、不同的垃圾回收器概述

2.1、经典垃圾回收器分类

  • 串行回收器:Serial、Serial Old(同一时间段内仅允许一个CPU用于执行垃圾回收操作)
  • 并行回收器:ParNew、Parallel Scavenge、Paraller Old(运用多个CPU同时执行垃圾回收)
  • 并发回收器:CMS、G1(回收垃圾线程和用户线程并发执行)

2.2、垃圾回收器与垃圾分代的关系

2.3、垃圾回收器的组合关系

2.4、查看默认垃圾回收器

  • -XX:+PrintCommanLineFlags:查看命令行相关参数
  • Jinfo –flag 垃圾回收器参数 进程ID

3、Serial回收器(串行回收)

3.1、Serial

  • JDK1.3前回收新生代的唯一选择,目前是HotSpot中Client模式下的默认新生代垃圾回收器
  • Serial采用复制算法、串行回收、STW机制

3.2、Serial Old

  • Serial收集器提供针对老年代的Serial Old收集器,采用标记-压缩算法、串行回收、STW机制。Serial Old是运行在Client模式下的默认老年代垃圾回收器,在Server模式下的作用是:
  1. 与新生代的Parallel Scavenge配合使用;
  2. 作为老年代CMS收集器的而后备垃圾收集方案

3.3、串行回收

  • 不仅理解为使用一个CPU或者一个线程完成GC工作,更理解为GC时需要暂停其他所有线程的工作STW(仅单核使用,单核最优最棒
  • 优势:简单而高效,没有与其他线程的交互,专注GC,所以运行在Client是不错滴
  • 内存几十到一两百MB合适,在短时间(几十ms到一百多ms)
  • 使用-XX:UseSerialGC指定年轻代和老年代都是用串行收集器,新生代使用Serial、老年代使用Old Serial
  • 对于交互性强的应用,不合适使用Serial,所以Java web中不采用

4、ParNew回收器(并行回收器,多线程GC)

  • Par是Parallel(并行)的简写,New表示针对新生代
  • 采用并行(多线程同时GC),采用复制算法、STW,除了并行之外,和Serial一致
  • Server模式下新生代默认垃圾收集器(但是目前慢慢过时了)

  • 新生代中回收次数频繁,所以使用并行回收,比较高效
  • 对于老年代,回收次数少,使用串行能节省资源,省去切换线程的资源
  • ParNew并行一定快吗?
  1. 在多核下,可以充分利用CPU,速度较快
  2. 在单核下,ParNew效率是比不过Serial的,因为要切换线程
  • 使用:
  1. 通过-XX:UseParNewGC手动指定使用ParNew垃圾回收器,仅在新生代使用
  2. 使用-XX:ParallerGCThreads可以设置ParNewGC使用的线程数量,默认情况下使用线程数量与CPU核数一样,可以将其设置为小于CPU核数,不建议大于

5、Parallel回收器(吞吐量优先)

5.1、Parallel Scavenge

  • 针对年轻代,使用复制算法、并行回收、STW
  • ParNew不同的是,Parallel Scavenge的目标是达到一个可控制的吞吐量,也就是吞吐量优先,且Parallel Scavenge采用自适应调节策略?
  • 高吞吐量意味着高效利用CPU,主要适合在后台运行、没有太多交互的任务,比如批量处理、订单处理、工资支付、科学计算等

5.2、Parallel Old

  • 在JDK1.6提供,用来替代老年代的Serial Old
  • 针对老年代,采用标记-压缩算法并行回收STW

  • 在吞吐量优先的场景中,Parallel Scavenge(高吞吐)和Parallel Old组合是Server模式下内存回收的合适选择
  • Java8中 默认使用Parallel

5.3、使用参数

  • -XX:+UseParallerGC手动指定年轻代使用Parallel Scavenge
  • -XX:+UseParallerOldGC手动指定老年代使用Parallel Old,-XX:+UseParallerGC和-XX:+UseParallerOldGC,启动了其中一个,那么另一个也会开启,相互激活
  • -XX:+ParallerGCThreads,设置年轻代并行垃圾收集器线程数,一般最好与核数相等。
  1. 默认情况下,核数小于8,那么ParallerGCThreads数量为8
  2. 核数大于8,那么ParallerGCThreads=3+[5*核数]/8
  • -XX:MaxGCPauseMillis设置GC最大STW时间,单位毫秒
  • -XX:GCTimeRatio设置垃圾收集时间占比
  1. 取值范围是(0,100),默认是99,说明GC时间是1%
  2. 与-XX:MaxGCPauseMillis有矛盾性
  • -XX:+UseAdaptiveSizePolicy设置Parallel Scavenge收集器是否具有自适应调节策略
  1. 开启时,老年代占比、Eden区占比、晋升老年代Age都会自动调整,在堆大小、吞吐量、停顿时间之间达到平衡
  2. 手动调优困难,直接使用即可,在仅指定了虚拟机最大堆、目标吞吐、和STW时间后,让JVM自己调优

6、CMS回收器(主打低延迟,并发)

6.1、介绍

  • 真正意义上的并发收集器Concurrent-Mark-Sweep(并行标记扫描)
  • 实现垃圾收集线程与用户线程同时工作
  • 主要缩短STW时间,应用于强交互应用,比如B/S系统的服务器、互联网网站等注重服务响应速度的应用
  • 采用标记-清除算法,其实也会有STW,只是时间少
  • CMS作为老年代的收集器,无法与Parallel Scavenge(JDK1.4,高吞吐)配合,所以只能选择ParNew(并行)或Serial(串行)
  • 在G1出现前,CMS应用广泛

6.2、CMS工作原理(四个阶段)

  • 初始标记,所有工作线程都要STW,仅仅标记GC Root直接关联的对象,速度很快,所以STW时间不长
  • 并发标记,用户线程不停,这时候从GC Root直接关联对象开始遍历整个对象图,耗时较长
  • 重新标记,在并发标记中,GC线程和用户线程同时工作,所以重新标记是为了修正并发标记期间产生变动的对象,耗时比初始阶段较长,但是比并发短得多,重新标记时需要STW,STW的时间比初始标记的长一点
  • 并发清除,清除掉标记为死亡的对象,释放空间,不需要移动存活对象(无压缩),与用户线程并发执行

6.3、总结

  • CMS在初始化标记和再次标记阶段需要STW,不过时间不长
  • 耗时最多的并发标记和并发清理时不需要STW,所以低停顿
  • 由于在清理阶段GC线程和用户线程并发执行,所以需要确保用户线程有足够空间,所以CMS不必等到没有空间了才GC,可以设置阈值,内存占用达到阈值边回收。如果CMS期间预留的内存用户线程,那么出现“Concurrent Mode Failure”失败,并启动后备方案:启用Serial Old收集器进行老年代垃圾回收,但是这样STW时间长了
  • 使用标记-清除算法,所以会产生碎片,只能使用空闲列表执行内存分配(不能使用标记-压缩算法,因为压缩的时候用户线程无法工作

6.4、优缺点

  • 优点:并发收集、低延迟
  • 缺点:
  1. 产生内存碎片
  2. CMS对cpu资源敏感,因为与用户线程并发,所以用户线程虽然不会STW,但是会变慢
  3. 无法收集处理浮动垃圾,会出现Concurrent Mode Failure失败,导致Full GC。
  4. 如果在并发标记阶段产生新的垃圾,CMS无法标记这些垃圾,最终这些垃圾不会被及时回收。

6.5、使用

  • -XX:+UseConcMarkSweepGC手动指定CMS,同时会开启-XX:+UseParNewGC,即同时使用ParNew(年轻代)+CMS(老年代)+Serial Old(老年代备用)
  • -XX:CMSlnitiatingOccupanyFraction设置堆内存使用率阈值,达到该阈值就回收
  1. JDK5默认值为68%,JDK6及以上默认92%
  2. 如果内存增长慢,可以设置阈值大一点,降低CMS触发频率
  3. 如果内存增长快,那么设置阈值小一点,多执行CMS,不执行Full GC(Serial Old备用)
  • -XX:+UseCMSCompactAtFullCollection用于设置Full GC后是否要压缩整理内存,-XX:CMSFullGCsBeforeCompaction设置多少次Full GC后进行压缩整理
  • -XX:ParallelCMSThreads设置CMS线程数量
  1. 默认是(ParallelGCThreads+3)/4
  2. ParallelGCThreads是年轻代并行收集器ParNew的线程数
  3. 当CPU资源紧张的时候,加上CMS的影响,用户程序性能会下降很多

6.6、新特性

  • JDK9以后,CMS被标记为Deprecate(不赞成、废弃),如果在JDK9以后开启-XX:+UseConcMarkSweepGC,用户会受到一个警告信息,提示CMS未来会被放弃
  • JDK14新特性:删除了CMS,如果在JDK14中使用-XX:+UseConcMarkSweepGC,不会报错,但是会给warning信息,不会exit,然后使用默认GC方式启动JVM

7、G1回收器(区域分代化)

7.1、G1-Garbage First

  • G1是并行回收器,将内存分割为多个不相关的Region
  • 避免进行整堆回收,根据每个Region的价值大小进行回收,在后台维护一个优先列表,每次根据设定的收集时间,优先回收价值最大的Region
  • 侧重于回收垃圾最大量的区间(Region),所以叫做垃圾优先Garbage First

  • 面向服务端,真多配备多核、大容量内存的机器,极力满足GC的同时兼顾高吞吐量
  • JDK7正式启用,JDK9中称为默认,取代了CMS以及Parallel+ Parallel Old组合,被称为全功能垃圾回收器

7.2、特点(优势)

  • 并行与并发
  1. 并行:多个GC线程同时工作
  2. 并发:G1可以与应用程序交替执行,部分GC工作可以和应用程序同时执行,不会在整个回收阶段发生STW
  • 分代收集
  1. 会区分年轻代和老年代,年轻代依旧分为Eden区和S区。不要求Eden、S、老年代是连续的,不坚持Region大小和数量
  2. 将堆分为若干Region,Region中包含逻辑上的年轻代和老年代,可同时在老年代、年轻代工作,不再区分
  • 空间整合
  1. CMS中是使用“标记-清除”算法,产生内存碎片,在经过若干次GC后进行碎片整理
  2. G1将内存划分为多个Region,GC时以Region为单位,Region之间使用复制算法,整体上可看做“标记-压缩”算法,避免了内存碎片
  • 可预测的停顿时间模型(软实时soft real-time)
  1. 使用可以确定在时间M内,花在GC时间不超过N
  2. 由于分区,G1仅在部分Region进行GC,缩小GC范围
  3. 根据各个Region里面垃圾的价值,在后台维护一个优先列表,每次根据允许的收集时间,优先手机回收价值最大的Region,保证G1在有限时间内获得尽量高的收集效率
  4. 与CMS相比,G1不一定能最好的解决STW,但是一定不是最差,而且还比较好

7.3、缺点

  • 相对于CMS,目前G1还没有形成压倒性优势,因为在用户程序运行过程中,G1在内存占用和程序运行时的额外执行负载都比CMS高
  • 在小堆内存上,CMS有优势,在大内存上G1更好,平衡点在6-8G

7.4、使用参数:

  • -XX:+UseG1GC:JDK9前(不含JDK9)手动设置,JDK9以后默认了
  • -XX:+G1HeapRegionSize:设置Region大小,值是设置值的二次幂,范围是1MB-32MB,大约划分2048个区域,默认是堆的1/2000
  • -XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间(尽量满足,不一定可达),默认为200ms
  • -XX:ParallelGCThread设置STW工作线程数(GC线程数),最多为8
  • -XX:ConcGCThread并发标记线程数,一般设置为GC线程数的1/4
  • -XX:InitiaingHeapOccupancyPercent设置触发GC周期的Java对占用率。超过此值就GC,默认为45

7.5、G1回收器的常见操作步骤

  • G1的设计原则是简化开发人员JVM调优,仅需三步设置即可:
  1. 开启G1垃圾收集器(JDK9以后是默认的)
  2. 设置堆最大空间(-Xmx)
  3. 设置最大停顿时间STW

7.6、G1适用场景

  • 服务端、大内存、多核处理器
  • 需要低延迟,少STW(每次只清理部分Region)
  • 可以替换CMS,以下情况下会比CMS好:
  1. 超过50%的Java堆被活动数据占用
  2. 对象更迭频率高、age变化快(变化快了,进老年代就快啦)
  3. GC停顿时间过长,G1可以降低STW时间
  • 其他的垃圾收集器都是使用JVM内置线程进行GC;G1可以使用应用线程承担后台的GC工作,即当JVM的GC处理速度慢时,系统可以用应用线程帮助加速进行GC

7.7、Region

  • 将Java堆划分为大约2048个大小相同的独立Region,块的大小根据堆大小决定,控制在1MB~32MB(2的N次幂),可通过参数-XX:G1HeapRegionSize设定
  • 依旧保留老年代和新生代概念,但是老年代新生代之间不再隔离,各自之间也不需要连续,通过动态分配的方式保证其逻辑上是连续的
  • 一个Region可能是Eden、S、老年代,但是Region只能是其中之一
  • 新增Humongous区用于存储大对象,一个对象超过0.5个Region,就存到H
  • 设置H区:
  1. 存储短期大对象
  2. 如果如果一个H区存不下,那么找连续的H区来存,为了找连续的H区,有时候可能要Full GC
  • 通过指针碰撞(Bump The Pointer)检测
  • 线程可私有Region,类似TLAB

7.8、G1垃圾回收过程

  • 收集年轻代 -> 老年代并发标记过程 -> 混合回收(循环回收)(如果有需要,单线程、独占式、高强度的Full GC继续存在,提供一种失败保护机制)

  • 当Eden区用尽时开始年轻代回收过程:
  1.  G1年轻代收集器是并行的独占式收集器
  2. 年轻代回收时,暂停所有用户线程,启动多线程执行年轻代垃圾回收
  3. 然后从年轻代区间移动存活对象到S区或老年区,也有可能同时涉及
  • 老年代回收:当堆内存使用达到一定值(默认45%),开始老年代并发标记
  • 混合回收:
  1. 老年代并发标记工作完成后立刻进行混合回收过程
  2. 从老年区移动存活对象到空闲区,从而空闲区也变成老年代一部分
  3. G1的老年代回收器不会整个回收老年代,一次仅扫描回收一部分老年代的Region
  4. 同时,老年代Region和年轻代是一起被回收的

7.9、Remembered Set

  • 要解决的问题:
  1. 一个对象被不同区域(老年代、年轻代)引用
  2. Region不是孤立的,一个Region中的对象可以被任意Region中的对象引用,判断Region时是否扫描所有Region?
  3. 回收新生代,新生代中对象被老年代引用,那么也得扫描老年代?
  • 解决的办法
  1. 无论是G1还是其他分代收集器,都是用Remembered Set
  2. 每个Region都有一个Remembered Set
  3. 每次Reference引用类型数据写操作时,要产生Write Barrier暂时中断,然后检查将要写入的引用指向的对象是否和该Reference对象在不同的Region
  4. 如果在不同的Region,需要CardTable把相关引用引用信息记录到引用指向对象的所有Region对应的Remembered Set中(就是谁引用了我Region中的对象,就在我这个Region的记忆Set中保存你)
  5. 如果同一个,就不用记录啦
  6. 进行垃圾回收时,在GC根节点的枚举范围加入Remembered Set,就可以不进行全局扫描了(就能知道谁引用谁了)

7.10、年轻代GC回收

  • 程序运行时不断有对象进入Eden,Eden耗尽时启动年轻代垃圾回收
  • 年轻代垃圾回收仅收集Eden区和S区
  • 年轻代GC在工作时,要STW,G1会创建回收集(Collection Set)回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含Eden区和S区所有内存分段
  • 回收过程:
  1. 根扫描:除了扫描GC Root外,还要扫描Remembered Set(RSet)
  2. 更新RSet:处理dirty card queue中的card,更新RSet(dirty card queue?)
  3. 处理RSet:识别被老年代对象指向的Eden对象(被指向的是活的)
  4. 复制对象:遍历对象树,Eden区中存活的复制到S区,S区中年龄不够则+1,够了就复制到老年代。如果S区不够,对象直接晋升老年代
  5. 处理引用:处理Soft、Weak、Phantom、Final、JNI Weak等引用。最后Eden区为空,GC停止,且目标内存中的对象连续,无碎片。

7.11、老年代并发标记过程(仅回收完全空闲的Region

  • 初始标记阶段:标记从根节点可以直接到达的对象,STW,且要触发一次年轻代GC
  • 根区域扫描:扫描S区可以直达老年代的对象,并标记为被引用对象,在Young GC之前完成
  • 并发标记:在整个堆中进行并发标记(和应用程序一起执行),可能被Young GC中断。在此极端,如果发现Region中的对象全部是垃圾,那么这个区域直接回收
  • 再次标记:由于应用程序在执行,所以需要修正并发标记结果,需要STW,G1采用比CMS更快的初始快照算法snapshot-at-the beginning(SATB)
  • 独占清理:计算各Region的存活对象和回收比例,进行排序,识别可以混合回收的对象,STW(此时还不会回收)
  • 并发清理阶段:识别并清空完全空闲的Region

7.12、混合回收

  • 除了回收Young Region,还会回收部分Old Region
  • 并发标记后,老年代中完全是垃圾的Region被回收了,部分为垃圾的被计算了。默认情况下部分为垃圾的Region会分8次被回收(可通过-XX:G1MixedGCCountTarget设置)
  • 混合回收大回收集包含1/8的老年代内存分段,Eden去内存分段,S区内存分段。混合回收算法和年轻代回收算法一直,只是多了1/8的老年代内存分段
  • 由于分8次回收,G1会优先回收垃圾多的内存分段,垃圾占比越高的Region,越先回收。还可以设置一个阈值决定是否可以回收,默认是65%(-XX:G1MixedGCLiveThresholdPercent),这样可以不回收存活对象多的Region(存活多,复制要时间呀)
  • 其实也不一定进行8次回收,有一个阈值,默认为10%(参数-XX:G1HeapWastePercent)设置,允许有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内的比例低于10%,那么停止混合回收(因为浪费时间GC,但是回收的内存确很少)

7.13、可选阶段:Full GC

  • G1就是为了避免Full GC
  • 如果G1无法正常工作,那么G1会STW,使用单线程内存回收算法进行垃圾回收,性能差,且STW长(就是使用了Full GC)
  • 如何避免:增大内存,堆小的时候,G1在复制存活对象的时候没有空余的内存分段可以使用,那么就会Full GC
  • Full GC的两个可能原因:
  1. Evacation时没有足够的to-space存放晋升对象
  2. 并处理过程完成前内存耗尽

7.14、优化G1建议

  • 年轻代大小
  1.  避免使用-Xmn或-XX:NewRatio设置年轻代大小
  2. 固定年轻代大小了会覆盖暂停时间目标(STW目标)
  • 设定STW时间不能过于严苛
  1.  G1目标是90%应用时间,10%GC时间
  2. STW时间设置得过于苛刻,会影响吞吐量

8、垃圾回收器总结

  • 截止JDK1.8,有七款典型GC回收器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值