Java虚拟机 垃圾回收器
垃圾回收器
- 如果说垃圾收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者
-
GC分类与性能指标
-
垃圾回收器分类
- 按垃圾回收线程数(串行和并行)
![]()
串行垃圾回收器- 串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束
- 在单CPU处理器或者较小应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以串行回收默认被应用在客户端的client模式下的JVM中
- 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
- 串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束
- 并行垃圾回收器
- 和串行相反,并行收集可以运用在多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
- 按照工作模式分(并发式和独占式)
- 并发式
- 垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间
- 独占式
![]()
-
- 一旦运行,就停止应用程序中所有的用户线程,直到垃圾回收过程完全结束
- 并发式
- 按照碎片处理方式(是否压缩内存)
- 压缩式
- 非压缩式
- 按个工作内存区间分
- 年轻代
- 老年代
- 按垃圾回收线程数(串行和并行)
-
性能指标
- 吞吐量
- 运行用户代码的时间占总运行时间的比例
- 总运行时间:程序的运行时间 + 内存回收的时间
- 吞吐量优先,意味着单位时间内,STW的时间最短
- 垃圾收集开销
- 吞吐量的补数,垃圾收集所占用的时间与总运行时间的比例
- 暂停时间
- 执行垃圾收集时,程序的工作线程被暂停的时间
- 暂停时间优先,意味着单次STW的时间最短,但是频率可能增加
- 收集频率
- 相对于应用程序的执行,收集操作发生的频率
- 内存占用
- Java堆区所占的内存大小
- 快速
- 一个对象从诞生到被回收经历的时间
- 吞吐量
-
不可能三角
- 简单来说抓住两点:吞吐量和暂停时间
- 高吞吐量与低暂停时间,是一对互相竞争的。因为如果高吞吐量优先,必然需要降低内存回收的执行频率,导致GC需要更长的暂停时间来执行内存回收
- 如果选择低延迟优先为原则,也只能频繁的执行内存回收,引起程序吞吐量的下降
- 普遍的标准,在最大吞吐量优先的情况下,降低停顿时间
-
-
不同的垃圾回收器概述
-
垃圾回收器的发展迭代史
- Serial GC
- 1999年JDK1.3.1
- 第一款GC
- ParNew
- 是SerialGC收集器的多线程版本,主要是配合CMS使用
- Parallel GC和Concurrent Mark SweepGC
- JDK1.4.2
- 2002年2月26日
- ParallelGC在JDK1.6之后称为HotSpot默认的GC
- G1
- 2012年
- JDK1.7u4
- 2017年JDK9中G1变成默认的垃圾收集器,以替代CMS
- 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性改善最坏情况下的延迟
- Epsilon 垃圾回收器、ZGC,可伸缩低延迟垃圾回收器
- 2018年9月JDK11
- Shenandoah GC:低停顿时间的GC,实验版
- 2019年3月JDK12
- 增强ZGC
- 2019年9月JDK13
- 删除CMS垃圾回收器,扩展ZGC在macOS和Windows上的应用
- 2020年3月JDK14
- Serial GC
-
7款经典垃圾收集器和垃圾分代之间的关系
- 垃圾收集器的组合关系
![]()
-
- JDK8之前,可以用虚线参考关系
- CMS下面的实线,是CMS回收失败的后备方案
- JDK8中取消了红线的组合,标记为废弃的。如果要用也可以用
- JDK9中将红线做了remove
- JDK9中标记CMS为废弃的
- JDK14中弃用了绿线组合
- JDK14中删除了CMS
- JDK9默认使用G1
- JDK8默认使用Parallel Scavenge、Parallel Old GC。新生代用了Parallel Scavenge则老年代自动触发用Parallel Old
- Parallel底层与ParNew底层不同,所以不能和CMS组合
- 总结一下随着JDK版本的更新,出现了不同垃圾收集器且组合之间关系也发生变化
- Client模式下默认使用Serial GC和Serial Old GC,JDK8默认使用Parallel Scavenge和Parallel Old GC,JDK9默认使用G1,JDK15默认使用ZGC
-
如何查看默认的垃圾收集器
- -XX:+PrintCommandLineFlags
- jinfo -flag 相关垃圾回收器参数 进程ID
- 演示jinfo -flag命令
![]()
-
-
Serial回收器:串行回收
- Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
- 除了年轻代,还有用于执行老年代的Serial Old收集器,同样采取了串行回收,但是用标记压缩算法
- 执行流程图
![]()
- 使用一个CPU或者一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有工作线程
- 优势
- 简单而高效,对于限定单个CPU的环境来说,由于没有线程交互的开销,可以获取最高的单线程收集效率
- HotSpot虚拟机中,使用-XX:+UseSerialGC指定年轻代和老年代使用串行收集器
- 对于交互强的应用而言,不会采取串行垃圾收集器
-
ParNew回收器:并行回收
- 除了采用并行回收,其他方面和Serial之间几乎没有任何区别
- 执行流程图
![]()
- -XX:UseParNewGC手工指定ParNew收集器执行内存回收任务,它表示年轻代使用,不影响老年代
- -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
- 除了采用并行回收,其他方面和Serial之间几乎没有任何区别
-
Parallel回收器:吞吐量优先
- 也是并行回收
- 和ParNew不同,它的目标是达到一个可控制的吞吐量
- 自适应调节策略也是Parallel与ParNew的一个重要区别
- 适合后台运算不需要太多交互的任务,例如执行批量处理、订单处理、工资支付、科学计算的应用程序
- Parallel Old采取标记压缩算法,同样基于并行回收和STW机制
- 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。在JDK8中是默认的垃圾收集器组合
- 执行流程
![]()
- 参数配置
- -XX:+UseParallelGC
- 手动指定年轻代使用此收集器执行内存回收任务
- -XX:+UseParallelOldGC
- 手工指定老年代使用并行回收收集器,分别适用于新生代和老年代,默认JDK8是开启的
- 与上面这两个参数关联,开启一个,默认开启另一个
- -XX:ParallelGCThreads
- 设置年轻代并行收集器的线程数,一般与CPU数量相同,如果CPU数量大于8个,则值 = 3 + (5 * N / 8)
- -XX:MaxGCPauseMillis
- 设置收集器最大停顿时间,单位毫秒
- 该参数谨慎使用
- -XX:GCTimeRatio
- 垃圾收集占总时间比,用于衡量吞吐量大小
- 默认99,取值范围0-100,也就是垃圾回收时间不超过1%
- 与上一个参数矛盾,暂停时间越长,Ratio参数就容易超过设定比例
- 垃圾收集占总时间比,用于衡量吞吐量大小
- -XX:+UseAdaptiveSizePolicy
- 开启自适应调节策略
- 这种模式下,年轻代大小,Eden和Survivor的比例,晋升老年底对象年龄参数都会被自动调整
- 为了达到堆大小、吞吐量和停顿时间之间的平衡点
- 在手动调优比较困难的场景下,可以直接用自适应方式,仅指定虚拟机最大堆,目标吞吐量和停顿时间,让虚拟机自己完成调优工作
- 开启自适应调节策略
- -XX:+UseParallelGC
-
CMS回收器:低延迟
- JDK1.5推出Concurrent Mark Sweep并发的标记清除垃圾收集器,工作在老年代,单独回收老年代,第一次实现了让垃圾收集线程与用户线程同时工作
- 执行流程图
![]()
- 初始标记:STW,仅仅只是标记出GC Roots能直接关联的对象,一旦标记完成后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里速度非常快
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
- 重新标记:为了修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录(增量更新,这一步需要STW)
- 并发清除:清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
- 初始标记和重新标记阶段仍然需要STW机制
- 由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收
- 要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这时虚拟机启用备用方案,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就长了
- CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
- 为什么不采取标记压缩呢?
- 因为并发清除时,如果用压缩整理内存,对象的地址就会发生变化,用户线程使用对象就会发生错误
- 标记压缩更适合STW场景下使用
- 优点
- 并发收集
- 低延迟
- 缺点
- 会产生内存碎片
- 对CPU资源非常敏感
- 在并发阶段会占用一部分线程导致应用程序变慢
- 无法处理浮动垃圾
- 并发标记阶段是与工作线程同时运行,如果并发阶段产生垃圾对象,CMS无法进行标记,导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
- 参数
- -XX:+UseConcMarkSweepGC
- 手工指定CMS收集器执行内存回收任务
- 开启后,自动将-XX:UseParNewGC打开,即ParNew(Young区)+ CMS(Old区)+ Serial GC组合
- -XX:CMSlnitiatingOccupanyFraction
- 设置堆内存使用率的阈值
- 一旦达到该阈值,则开始进行回收
- JDK5及之前默认68,即老年代的空间使用率达到68%时会执行一次CMS回收
- JDK6及以上默认值为92%
- 如果内存增长缓慢,可以设置一个稍大的值,有效降低CMS的触发频率,减少老年代回收的次数
- 如果应用程序内存使用率增加很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器
- 设置堆内存使用率的阈值
- -XX:+UseCMSCompactAtFullCollection
- 用于执行完Full GC后对内存空间进行压缩整理
- 不过内存压缩无法并发执行,会带来停顿时间更长的问题
- -XX:CMSFullGCsBeforeCompaction
- 设置执行多少次FullGC后对内存空间进行压缩整理
- -XX:ParallelCMSThreads
- 设置CMS的线程数量
- 默认启动的线程数是(ParallelGCThreads+3) / 4
- ParallelGCThreads是年轻代并行收集器的线程数
- 设置CMS的线程数量
- -XX:+UseConcMarkSweepGC
- 小结
- 如果想要最小化使用内存和并行开销,选择Serial GC
- 如果最大化应用程序的吞吐量,选择Parallel GC
- 如果想要最小化的GC的中断或停顿时间,选择CMS GC
- CMS在JDK9被标记为废弃的,在JDK14正式删除
- JDK1.5推出Concurrent Mark Sweep并发的标记清除垃圾收集器,工作在老年代,单独回收老年代,第一次实现了让垃圾收集线程与用户线程同时工作
-
G1回收器:区域化分代式
- G1详细解释 官方给G1设定的目标
- 就是在延迟可控的情况下,获得尽可能高的吞吐量,所以才担当起全功能收集器的重任和期望
- Garbage First
- G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续)
- 使用不同的region表示Eden、S0、S1、老年代等
- G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
- JDK7版本正式启用,JDK9以后默认垃圾回收器
- JDK8还不是默认的,需要用-XX:+UseG1GC参数来启用
- 优势
- 并行与并发
- 分代收集
- 同时兼顾年轻代与老年代
- 空间整合
- region之间用复制算法,整体可以看做是标记压缩算法
- 两种算法都避免内存碎片,有利于程序长时间运行,分配大对象不会因为无法找到连续空间提前触发下一次GC,尤其当Java堆非常大的时候,G1优势更加明显
- 可预测的停顿时间模型
- 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
- 缺点
- 相较于CMS,G1不具备全方位,压倒性优势。比如用户程序运行中,G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
- 经验上来说,小内存应用CMS表现大概率优于G1,在大内存上G1优势发挥更多,平衡点在6 - 8GB
- 常见参数设置
- -XX:+UseG1GC
- 启用G1垃圾收集器
- -XX:G1HeapRegionSize
- 设置每个Region大小,值是2的幂次方,范围是1MB到32MB之间,目标是根据最小的Java堆划分出约2048个区域,默认是堆内存的1 / 2000
- -XX:MaxGCPauseMillis
- 设置期望达到的最大GC停顿时间指标,JVM尽力但不保证,默认200ms
- -XX:ParallelGCThreads
- 设置STW工作线程数的值,最多设置8
- -XX:ConcGCThreads
- 设置并发标记的线程数,将N设置为并行垃圾回收线程数(ParallelGCThreads)的1 / 4左右
- -XX:InitiatingHeapOccupancyPercent
- 设置触发并发GC周期的Java堆占用率阈值,超过此值就触发GC,默认是45
- -XX:+UseG1GC
- 常见调优
- 第一步开启G1垃圾收集器
- 第二步设置堆的最大内存
- 第三步设置最大的停顿时间
- G1提供了三种垃圾回收模式在不同的条件下触发
- YoungGC
- MixedGC
- FullGC
- 适用场景
- 面向服务器端应用,针对具有大内存,多处理器的机器
- 最主要应用是需要低GC延迟
- 如:在堆大小约6GB或更大,可预测的暂停时间可以低于0.5s,G1每次清理一部分region来保证每次GC停顿时间不会过长
- 用来替换1.5中的CMS
- 超过50%的Java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间过长,长于0.5~1秒
- region
- 所有region大小相同,且在JVM生命周期内不会改变
- 结构图
![]()
- region可以充当多个角色
- 所有region大小相同,且在JVM生命周期内不会改变
- 垃圾回收过程
- 年轻代GC(STW)
- 当年轻代eden区用尽时
- 并行独占式收集器
- 当年轻代eden区用尽时
- 老年代GC,并发标记过程
- 当堆内存使用到一定值,默认45%
- 混合回收
- 标记完成马上开始混合回收
- G1老年代回收器不需要整个老年代都被回收,一次只需要扫描回收一小部分老年代的region就可以了
- 同时老年代是和年轻代一起被回收的
- 有可能FullGC
- 年轻代GC(STW)
- 记忆集
- 每个region对应一个记忆集
- 通过记忆集避免全局扫描
- 每次引用类型数据写操作时,会产生一个写屏障暂时中断操作
- 然后检查将要希尔的引用指向的对象是否和该引用对象类型数据在不同的region,如果不同就通过Card Table把相关的引用信息记录到引用指向对象所在的Region对应的记忆集中
- 当进行垃圾收集时,在GC根节点枚举范围加入记忆集,就可以保证不进行全局扫描,也不会有遗漏
- 每个region对应一个记忆集
- G1回收过程一:年轻代GC(STW)
- 1、扫描GC Roots
- 根是指static变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同Rset记录的外部引用作为扫描存活对象的入口
- 2、更新Rset
- 处理dirty card queue中的card,更新Rset,此阶段完成后,Rset可以准确的反应老年代所在的内存分段中对象的引用
- 3、处理Rset
- 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
- 4、复制对象
- 对象图被遍历,Eden区内存段中存活的对象会被复制到Survivor去中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,会加一,达到阈值会被复制到Old区中空的内存分段,如果Survivor区空间不够,Eden空间的部分数据会直接晋升到老年代空间
- 5、处理引用
- 处理强软弱虚,终结器引用,本地方法接口引用等,最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片
- 1、扫描GC Roots
- G1回收过程二:老年代GC
- 初始标记阶段(STW)
- 标记从根节点直接可达的对象,并且触发一次年轻代GC
- 根区域扫描阶段
- 扫描Survivor区直接可达老年代区域对象,并标记被引用的对象,这个过程在YoungGC之前完成
- 并发标记
- 和应用程序并发执行,并发标记阶段若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收
- 并发标记过程中,会计算每个区域的对象活性,存活对象的比例
- 再次标记
- 由于应用程序持续进行,需要修正上次标记结果,G1采取比CMS更快的原始快照算法(STAB)(STW)
- 独占清理
- 计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下个阶段做铺垫(STW)
- 这个阶段并不会实际上去做垃圾的收集
- 并发清理阶段
- 识别并清理完全空闲的区域(STW)
- 初始标记阶段(STW)
- G1回收过程三:混合回收
- 当越来越多的对象晋升到老年代Old Region时,为了避免内存被耗尽,虚拟机会触发一次混合的垃圾收集器,该算法除了回收整个Young Region,还会回收一部分的Old Region。也要注意Mixed GC并不是Full GC
- 并发标记结束后,老年代中百分百为垃圾的Region被回收了。部分为垃圾的内存分段被计算出来了,默认情况下,这些老年代的内存分段会分8次被回收,由-XX:G1MixedGCCountTarget参数设置
- 混合回收的回收集包括八分之一的老年代、Eden区内存分段、Survivor区内存分段
- 由于老年代中内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为65%。意思是垃圾占比达到65%才会被回收。如果垃圾占比比较低,意味存活对象较高,复制的时候花更多时间
- 混合回收不一定要进行8次,有一个阈值:-XX:G1HeapWastePercent
- 默认值是10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存比例低于10%,则不再进行混合回收,因为GC花费更多的时间,但是回收到的内存却很少
- G1可选过程四:FullGC
- G1初衷就是要避免FullGC,如果上述方式不能正常工作,G1会停止应用程序的执行。使用单线程的内存回收算法进行垃圾回收,性能非常差。应用程序停顿时间长
- 比如堆太小,当G1复制存活对象的时候没有空的内存分段可用,则会回退到FullGC
- 导致FullGC原因可能有两个
- 回收阶段的时候没有足够的to-space存放晋升的对象
- 并发处理过程完成之前空间耗尽了
- 优化建议
- 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
- 固定的年轻代大小会覆盖暂停时间目标
- 暂停时间目标不要太苛刻,太苛刻会影响吞吐量
- G1详细解释 官方给G1设定的目标
-
垃圾回收器总结
GC日志分析
- 参数
- -verbose:gc参数解析
- -XX:+PrintGCDetails参数解析
- 日志补充说明1
- 日志补充说明2
- GC日志详细解释
- 关于日志分析工具可以到后面的故障排查章节继续讲解
-
垃圾回收器的新发展
- Shenandoah GC测试结果
- 强项
- 低延迟时间
- 弱项
- 高运行负担下的吞吐量下降
- ZGC(染色指针、读屏障、不分代、内存多重映射)
- 在尽可能堆吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在10毫秒以内的低延迟
- 标记阶段和整理阶段都是并发的,移动对象的过程中也可以并发的
- GC过程分为初始标记、并发标记、并发预备重分配、并发重分配、并发重映射
- 除了初始标记是STW,其他地方几乎都是并发执行的
- Shenandoah GC测试结果
各位看官大佬,不足之处,多多批评指正,不胜感激!


















浙公网安备 33010602011771号