Java教程

Java垃圾回收器和实现机制

本文主要是介绍Java垃圾回收器和实现机制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、Java垃圾是什么?

垃圾是指运行程序中没有任何指针指向的对象,这些对象就是需要被回收的垃圾。如果不及时对这些占用内存的垃圾进行清理,这些垃圾会一直占用内存空间直到应用程序结束,被占用的空间无法被其他对象使用,甚至可能导致内存溢出。

二、Java垃圾回收器概述

从Java JDK早期版本至今,Java垃圾回收器有10款分别是Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1、Epsilon、Shenandoah、ZGC。

  • Serial GC 串行回收器,负责回收新生代的垃圾,1999年随JDK1.3.1版本一起发布,是该版本默认垃圾回收器。
  • Serial Old GC 串行回收器,负责回收老年代的垃圾和Serial GC组合一起工作,在JDK9版本已被移除。
  • ParNew 并行回收器,负责回收新生代的垃圾,可以和CMS、Serial Old GC组合一起工作。
  • Parallel Scavenge 并行回收器,负责回收新生代的垃圾,2002年2月26日Parallel GC和CMS随JDK1.4.2版本一起发布,在JDK1.6后成为默认回收器。
  • Parallel Old 并行回收器,负责回收老年代的垃圾,和Parallel Scavenge组合一起工作。
  • CMS(Concurrent Mark Sweep GC)并发回收器,负责回收老年代的垃圾,在JDK14版本中该回收器已被移除。
  • G1 并发回收器,负责回收新生代和老年代的垃圾,在JDK1.7u4版本中G1可用,在2017年JDK9版本成为默认的垃圾回收器。
  • Epsilon 2018年9月 JDK11版本引入该回收器。
  • ZGC 和Epsilon 同时期引入,在2020年3月随JDK14版本发布,ZGC在MAC和Windows平台上可用,之前只是在Linux平台支持。
  • Shenandoah GC 是JDK12版本新的垃圾回收器。

下图展示的是7款经典垃圾回收器的组合关系图
在这里插入图片描述

三、Serial回收器

Serial回收器是最基本历史最悠久的垃圾回收器,在jdk1.3之前回收器新生代唯一选择。以下是Serial回收器工作原理图。
在这里插入图片描述
Serial回收器只能单线程工作,使用标记-复制算法和stop-the-world机制负责回收新生代垃圾,在回收工作时会暂停所有用户线程。Serial Old回收器也是单线程工作,使用标记-整理算法和stop-the-world机制负责回收老年代垃圾。它主要有两个用途:1.与新生代Parallel Savenge GC配合使用。2.作为老年代CMS收集器的后备方案。

使用命令

在HostSpot虚拟机中,使用"-XX:+UseSerialGC"参数指定年轻代使用Serial GC,老年代使用Serial Old GC。

应用场景

由于Serial GC是一个单线程回收器,因此更适合应用在硬件资源配置低,仅限单核CPU的服务器上。

四、ParNew回收器

ParNew是一款并行回收器,除了使用多线程执行回收内存外和Serial GC之间几乎没有任何区别,以下是ParNew回收器工作原理图。
在这里插入图片描述
ParNew是一款并行回收器,使用标记-复制算法和stop-the-world机制负责回收新生代垃圾,在回收工作过程中会暂停用户线程。目前除了Serial Old,只能和CMS回收器配合使用。

使用命令

在HotSpot虚拟机中,使用"-XX:+UserParNewGC"参数开启使用ParNew回收器。
使用"-XX:ParallelGCThreads={value}"参数限制GC线程数量,默认开启和CPU核数相同的线程。

当CPU核数大于8时,此时会使用公式计算ParallelGCThreads值,该公式为:3 +[(5*CPU)/8]

应用场景

ParNew GC是并行回收器,在年轻代回收次数频繁情况下,使用该回收器效率会更高。同时也正是因是多线程版本,在多核CPU环境下,使用改回收器能够很好充分利用硬件资源优势,快速的完成垃圾回收提升程序吞吐量。

四、Parallel回收器

在HotSpot虚拟机中年轻代除了ParNew回收器是基于并行回收外,还有Parallel回收器,该回收器在JDK8版本默认使用。以下是Parallel回收器工作原理图
在这里插入图片描述
Parallel回收器使用标记-复制算法和stop the world机制,负责回收新生代的垃圾,在回收过程中会暂停所有用户线程。
Parallel Old回收器使用标记-整理算法和stop the world机制,负责回收老年代的垃圾,在回收过程中会暂停所有用户线程。

这里有一个问题已经有了ParNew回收器,为什么还需要Parallel?
和ParNew回收器不同,Parallel Scavenge收集器目标是达到一个可控制的吞吐量,因此也被称为吞吐量优先的垃圾回收器。此外还具有自适应调节策略(动态的调整堆内存分配情况)功能也是和ParNew回收器一个重要区别。

使用命令

  • "-XX:+UseParallelGC"参数开启使用Parallel年轻代垃圾回收器。
  • "-XX:+UseParallelOldGC"参数手动开启老年代垃圾回收器,默认JDK8是自动开启的。
  • "-XX:ParallelGCThreads={value}"参数设置年轻代回收器并行线程数,默认和cpu核数相等,当cpu大于8时使用公式为:3 +[(5*CPU)/8]计算该值。
  • "-XX:MaxGCPauseMillis"参数设置垃圾收集器最大停顿时间,单位是毫秒。为了尽可能可以把停顿时间控制在MaxGCPauseMillis范围内,回收器会调整堆的大小以及其他一些参数,JVM没有设置默认暂停时间目标值,GC的暂停时间主要取决于堆中实时数据的数据量。这里需要注意如果该值设置太小,堆内存会变小,进而会导致频繁GC回收,降低整体吞吐量
  • "-XX:+useAdaptiveSizePolicy"设置Parallel Scavenge收集器具有自适应调节策略。默认开启状态,在这种模式下,年轻代的大小Eden和Suvivor的比例,晋升老年代的对象年龄等参数会自动调整,已达到堆大小,吞吐量和停顿时间之间的平衡点。
  • "-XX:GCTimeRatio"设置吞吐量大小,它的值是一个 0-100 之间的整数,默认是99。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。

应用场景

Parallel回收器具有高吞吐量特性,可以高效的利用CPU资源,尽快完成程序运算任务,适合在服务器环境中使用。

五、CMS回收器

CMS全称Concurrent-Mark-Sweep,这是回收器是Hotspot虚拟机真正意义上的并发回收器,它第一次实现了GC线程和用户线程同时工作。CMS回收器的关注点是尽可能的缩短用户线程的停顿时间。以下是CMS工作原理图。
在这里插入图片描述
CMS的工作原理比较复杂,主要分四个阶段分别是初始标记、并发标记、重新标记、并发清除标记。
1.初始标记阶段(Initial-Mark),这个阶段所有用户线程会因stop-the-world机制而暂停,主要任务是标记GC Root直接关联的对象,一旦标记完成后就会恢复之前暂停的用户线程继续工作。
2.并发标记阶段(Councurrent-Mark),这个阶段任务是从GC Root的直接关联对象开始遍历整个对象图,这个过程耗时比较长,但是由于不需要停顿用户线程,可以和GC一起并发工作。
3.重新标记阶段(ReMark)阶段,由于并发标记阶段中,用户线程和GC线程同时运行因此需要修正并发标记期间因用户线程运行而导致标记产生变动的那一部分。这个过程因stop-the-word机制所有用户线程都会暂停,但是暂停时间会非常短。
4.标记清除阶段(Concurrent-Sweep),此阶段清除整理标记阶段判断已死亡的对象,释放内存空间。

使用命令

  • 使用"-XX:+UserConcMarkSweepGC"参数手动指定使用CMS回收器。
  • 使用"-XX:CMSInitiatingOccupanyFraction"设置堆内存使用率的阈值,一旦达到这个阈值就会开始垃圾回收。
  • 使用"-XX:CMSFullGCsBeforeCompaction"参数设置在执行多少次Full GC后会执行做一个压缩整理(默认:0,代表每次都会做)。
  • 使用"XX:+UseCMSCompactAtFullCollection"参数默认设置为true,表示用于指定在执行完FullGC后对内存空间进行整理,配合CMSFullGCsBeforeCompaction参数使用。
  • 使用"-xx:ConcGCThreads"参数设置cms并发标记线程数。

CMS优势

总的来说CMS具有并发回收,低延迟特点。因此非常适用于在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以为用户带来较好的体验。

CMS缺点

CMS回收器具有以下三个缺点:
1.会产生内存碎片导致并发清除后,用户线程可用的空间不足,在无法分配大对象的情况不得不其他产生full GC。
2.cms回收器对cpu资源非常敏感,在并发阶段它不会导致用户线程停顿但会因为站用了一部分线程而导致应用程序变慢,总吞吐量下降。
3.cms回收器会导致浮动垃圾,可能会出现Concurrent Mode Failure失败而导致另一次Full GC的产生。

Concurrent Mode Failure是cmd专有的日志,GC线程和用户工作线程同时工作,清理出来的老年代空间不足以存放由新生代晋升到老年代的对象,从而导致老年年代垃圾回收变成了Serial Old,导致用户线程停顿时间长,应用程序变慢。
什么是浮动垃圾?
浮动垃圾是指在并发标记阶段本来可达的对象,由于线程的作用变成了不可达了即产生了新的垃圾。
在并发标记阶段可能产生两种变动,第一种本来可达的对象,变成不可达。第二种是本来不可达对象,变成可达。对于一种情况产生的浮动垃圾,相比之下还是可以容忍的,重新标记不处理第一种情况是因为需要从GC Root开始遍历这样会给重新标记带来额外的开销。

五、G1回收器

G1是一款并发回收器,设计目标是延迟可控的情况下,获得尽可能高的吞吐量。下图所示是G1的内存结构图在这里插入图片描述
G1的堆内存被分割为很多相同大小的Region,这些Region物理上是不连续的,使用不同的Region来表示Eden、Survivor、Old、Humongous。

这里需要注意设置Humongous原因是由于对于堆中的大对象,默认直接会被分配到老年代,如果是一个短期存在的大对象就会对垃圾回收器造成影响。因此G1划分了Humoungous区专门用来存储大的对象,如果一个H区装不了一个大对象,G1会寻找连续的H区来存储,有时候为了找到这些连续的区域,也会导致Full GC。大多数时候H区也会被当做老年代的一部分。

为什么叫Garbage First?

首先G1有计划的避免了在整个Java堆中进行全区域的垃圾回收。G1会跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的Region,所以叫G1也就是垃圾优先Garbage First。

Remember Set 和 Card Table

G1引入Remember Set 主要是为了解决跨带引用问题,避免了扫描全部的 Old Regions,提高Young GC的回收效率。在 G1 GC 的内存结构中,每个 Region 都会有一个 RSet 维护并跟踪其他 Region 对本 Region 的引用,当 Region 中对象被移动时 RSet 也会更新引用。
Card Table实际上是就是由每个Region被分成了多个Card,如果一个老年代区CardTable中有对象指向年轻代区,就将它标记为Dirty_Card。然后通过cardTable把相关引用信息记录到引用指向对象所在region(Old区)对应remembered set中。

G1垃圾回收过程

G1 GC垃圾回收过程主要有三个环节分别是年轻代GC,老年代并发标记过程,混合回收。当年轻代的Eden区用内存用尽时,便开始年轻代垃圾回收过程,G1的年轻代是多线程并行回收,此时会暂停所有用户线程,将年轻代存活的对象移动到survivor区间或者老年区间。当内存使用达到一定值45%时,开始老年代的并发标记过程,标记完成后马上进行混合回收,在混合回收过程中会将老年区移动存活的对象到空闲的区,这些空闲区也会成为老年代的一部分。老年代G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代回收,一次只扫描回收一小部分老年代的Region就可以了,同时这个老年代Region是和年轻代一起被回收。

G1垃圾回收详细过程

Young GC

G1在进行YGC时只会收集Eden区和Servivor区,所有用户线程都会被暂停,具体过程如下:

  • 第一阶段,扫描根对象。
  • 第二阶段,更新RSet,处理dirty card queue中的card,更新RSet,此阶段完成后,RSet可以准确的反应老年代区对年轻代Region区对象的引用。
  • 第三阶段,处理RSet,识别老年代的对象指向年轻代Region的对象,这些指向年轻代的对象被认为是存活对象。
  • 第四阶段,复制对象,这个阶段对象树会被遍历,将Eden区存活的对象复制到Survivor区中空闲的空间,Survivor区中存活的对象年龄未达到阈值就会加一,达到阈值就会将对象复制到老年代区,如果Survivor区空间不足就会将部分对象晋升到老年代区。

并发标记过程

  • 初始化标记阶段,标记从根节点直接可达的对象,这个阶段会使用stop the word机制,同时会触发一次年轻代GC。
  • 根区域扫描,G1 GC扫描Survivor区直接可达的老年代区域的对象,并标记被引用的对象这一个过程必须在yong GC之前完成。
  • 并发标记在整个堆中进行并发标记,次过程可能被yong GC中断,在并发标记阶段,如发现区域所有对象都是垃圾,那么这个区域就会被回收。通过并发标记会计算每个区域对象存活比例。
  • 在次标记,由于用户线程和GC线程同步工作,需要修改上一次的标记结果,此时会STW。GQ采用比CMS更快的初始化快照算法SATB。
  • 独占清理,计算各个区域存活对象和GC回收比例,并进行排序识别可以混合回收的区域,此阶段也是stw,也是为下阶段做铺垫。
  • 并发清理,识别并清理完全空闲区域。

混合回收

当越来越多对象晋升到老年代Old region时,为了避免堆内存被耗尽,虚拟机会触发一次混合的垃圾回收即Mixed GC,该算法并不是一个Old GC除了回收整个Young Region还会回收一部分Old Region。G1可以选择那些Old Region进行回收,根据垃圾回收耗时时间进行空间,需要注意Mixed GC并不是Full GC。

在并发标记以后,老年代中内存分段的垃圾百分比被回收,部分为分段的垃圾被计算出来,默认情况下这些老年代的内存分段会被分8次回收,该参数可以通过XX:G1MixedGCCountTarget设置。
混合回收包括八分之一的老年代内存分段和新生代内存分段,回收方法和年轻代回收一样。
由于老年代中内存分段默认会被分8次回收,因此G1会优先回收垃圾多得内存分段,垃圾占比例越高就越有机会被回收,并且有一个阈值决定内存分段是否被回收,XX:G1MixedGCLiveThresholdPercent,默认是65%.。意思是垃圾占比达到65%才会被回收。
混合回收不一定进行8次,有一个参数xx:G1HeadWastePercent,默认值10%,意味着如果回收的垃圾比例占整个堆的内存少于10%,则不会进行垃圾回收。

G1优势和缺点

优势

G1 GC内存被分割为多个region,内存回收是以region为基本单位,region直接使用标记整理算法,避免了内存碎片,这种特性有利于程序长时间运行。此外G1相对于cms另个优化是,除了追求低停顿时间外,还建立可预测停顿时间模型,在明确一个m毫秒的时间内,消耗在垃圾收集器上的时间不得超过m毫秒,由于G1可以只选择部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也得到了很好的控制。

缺点

相比cms G1还不具备全方位,压倒性的优化,比如无论是在用户程序运行过程中G1在回收垃圾产生的额外执行负载占用内存会比cms高。从经验上来说,在内存应用比较小服务器上CMS的表现大概会优于G1,而G1在大内存应用上则发挥其优势,平衡点在6到8GB之间。

这篇关于Java垃圾回收器和实现机制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!