【JVM系列5】深刻分析Java垃圾收集算法和经常使用垃圾收集器

前言

上一篇咱们介绍了对象在堆内的内存布局已经占用空间的大小,同时分析了堆内能够分为Young区和Old区,并且Young区能够分为Eden区和Survivor区,Survivor区又拆分红了两个大小同样的区S0和S1区域,其实这么拆分的理由和GC是密切相关的,那么这一篇文章就让咱们深刻了解一下Java中的垃圾收集机制。java

如何肯定无效对象

在垃圾收集的时候第一件事就是怎么肯定一个对象是垃圾,那么该如何肯定一个对象已经能够被回收了呢?主流的算法有两种:引用计数法可达性分析算法算法

引用计数法(Reference Counting)

这个算法很简单,效率也很是高。就是给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时,计数器的值就减1,当计数器的值减为0时就表名这个对象不会再被使用,成为了无用对象,能够被回收。编程

这种算法虽然实现简单,效率也高,可是存在一个问题,咱们看下面一个场景:多线程

在这里插入图片描述
上图中4个对象相互引用,可是并无其余对象去引用他们,这种对象实际上也是无效对象,可是他们的引用计数器都是1而不是0,因此引用计数法没办法解决这种“一坨垃圾”的场景。并发

可达性分析算法(Reachability Analysis)

可达性分析算法就是选择一些对象做为起始点,这些对象称之为:GC Root。而后从GC Root开始向下搜索,搜索路径称之为引用链(Reference Chain),当一个对象不在任何一条引用链上时,就说明此对象是无效对象,能够被回收。
好比说下面这幅图,右边那一串互相引用的对象由于没有不在GC Root的引用链上,因此就是无效对象,可达性分析算法有效的解决了互相引用对象没法回收问题。
在这里插入图片描述jvm

GC Root

在Java中,能够做为GC Root对象的包括下面几种:工具

  • Java虚拟机栈内栈帧中的局部变量表中的变量
  • 方法区中类静态属性
  • 方法区中常量
  • 本地方法栈中JNI(即Native方法)中的变量

注意:在分析对象的过程当中,为了确保结果的准确性,须要保证分析过程当中对象引用关系不会发生变化,而为了达到这个目的,就须要暂停用户线程,这种操做也叫:Stop The World(STW)。布局

引用的分类

上面两种算法其实都是一个目的,判断对象有没有被引用,而引用也不只仅都是同样的引用,JDK1.2开始,Java中将引用进行了分类,划分红了四种引用,分别是:强引用,软引用,弱引用,虚引用。这四种引用关系的强度为:强引用>软引用>弱引用>虚引用。性能

强引用(Strong Reference)

咱们写的代码中通常都是用的强引用,如:Object obj = new Object()这种就属于强引用,强引用只要还存在,必定不会被回收,空间不够就直接抛出OOM异常spa

软引用(Soft Reference)

软引用是经过SoftReference类来实现的。软引用能够用来表示一些还有用但又是非必需的对象,系统在即将溢出以前,若是发现有软引用的对象存在,会对其进行二次回收,回收以后内存仍是不够,就会抛出OOM异常。

弱引用(Weak Reference)

弱引用是经过WeakReference类来实现的。弱引用也是用来表示非必需对象的,可是相比较软引用,弱引用的对象会在第一次垃圾回收的时候就被回收掉。

虚引用(Phantom Reference)

虚引用是经过PhantomReference类来实现的,也被成为幽灵引用或者幻影引用。这是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不对其生存时间构成影响。也没法经过虚引用来取得一个对象实例。设置为虚引用的惟一用处可能就是当这个对象被回收的时候能够收到一个系统通知。

垃圾收集算法

上面分析了如何肯定一个对象属于可回收对象的两种算法,那么当一个对象被肯定为垃圾以后,就须要对其进行回收,回收也有不一样的算法,下面就来看一下经常使用的垃圾收集算法

标记-清除(Mark-Sweep)算法

标记-清除算法主要分为两步,标记(Mark)和清除(Sweep)。
好比说有下面一块内存区域(白色-未使用,灰色-无引用,蓝色-有引用):
在这里插入图片描述
而后标记-清除算法会进行以下两个步骤:

  • 一、将堆内存扫描一遍,而后会把灰色的区域(无引用对象,可悲回收)对象标记一下。
  • 二、继续扫描,扫描的同时将被标记的对象进行统一回收。

标记清除以后获得以下图所示:
在这里插入图片描述
能够很明显看到,回收以后内存空间是不连续的,产生了大量的内存空间碎片。过多内存碎片最直接的就是能够致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

标记-清除算法的缺点

一、标记和清除两个过程都比较耗时,效率不高
二、会产生大量不连续的内存碎片。

为了解决这两个问题,因此就有了复制算法。

复制(Copying)算法

复制算法的思想就是把内存区域一分为二,两块内存保持同样的大小,每次只使用其中的一块,当其中一块内存使用完了以后,将仍然存活的对象复制到另外一块内存区域,而后把已使用的一半内存所有一次性清理掉。
以下图(绿色表示暂时不放对象的一半空间):
在这里插入图片描述
回收以后:
在这里插入图片描述

复制算法的缺点

复制算法的缺点就是牺牲了一半的内存空间,有点过于浪费。

复制算法在Java虚拟机的落地形式

Java堆内存中作了好几回划分,最后是将Survivor区分红了2个区域S0和S1来进行复制算法,这种作法就是为了弥补原始复制算法直接将一半的空间做为空闲空间方式的弥补。

IBM公司的研究代表,Young区(新生代)中98%的对象都是“朝生夕死”的,生命周期极短,因此说在一次GC以后能存活下来的对象不多,彻底不必划分一半的区间来进行复制算法。Hot Spot虚拟机中Eden区和Survivor区域的比例为:Eden:S0:S1=8:1:1,也就是说其实只有10%的空间被浪费掉,彻底是能够接受的。

标记-整理(Mark-Compact)算法

咱们想一下,假如Young区(新生代)的对象在一次GC以后,基本全部对象都存活下来了,那就须要复制大量的对象,效率也会变低。而堆中的old区(老年代)的特色就是对象生命周期极为顽强,由于默认要进行第16次垃圾回收的时候还能存活下来的对象才会放到老年代,因此对老年代中对象的回收通常不会选择标记-复制算法。

标记-整理算法就是为了老年代而设计的一种算法,标记-整理算法和标记清除算法的区别就是在最后一步,标记-整理算法不会对对象进行清理,而是进行移动,将存活的对象所有向一端移动,而后清理掉端边界之外的对象。以下图所示:
回收前:
在这里插入图片描述
回收后:
在这里插入图片描述

分代收集算法(Generational Collection)

目前主流的商业虚拟机都是采用的分代收集算法,这种算法本质上就是上面介绍的算法的结合体。新生代采用标记-清除算法,老年代采用标记-清除或者标记-整理算法。

垃圾收集器

上面介绍了肯定对象的算法以及回收对象的算法,而后具体要怎么落地却并无一个规定,而垃圾收集器就是实现了对算法的落地,而由于落地形式不一样,天然也产生了不少不一样的收集器。下面是一张收集器的汇总图:
在这里插入图片描述
上面一半表示新生代收集器,下面一半表示老年代收集器,横跨中间的表示均可以用。

根据这个图形有了总体认知以后,咱们再来一个个看看这些垃圾收集器的工做原理吧。

Serial和Serial Old收集器

Serial收集器是基本、发展历史悠久的收集器,在JDK1.3.1以前是虚拟机新生代收集的惟 一选择。
Serial收集器是一种单线程收集器,并且是在进行垃圾收集的时候须要暂停全部其余线程,也就是说触发了GC的时候,用户线程是暂停的,若是GC时间过长,用户是能够明显感知到卡顿的。
Serial Old是Serial的一个老年代版本,也是一种单线程收集器。
能够用下面一个图形来表示一下Serial和Serial Old收集器的工做原理:
在这里插入图片描述
优势:简单高效,拥有很高的单线程收集效率
缺点:收集过程须要暂停全部线程
算法:Serial采用复制算法,Serial Old采用标记-整理算法
适用范围:Serial用于新生代,Serial Old用于老年代
应用:Client模式下的默认的收集器

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,实现了并行执行,其他工做原理都和Serial一致。可使用参数:-XX:+UseParNewGC来指定使用。

注意:这里的并行指的是多个GC线程并行,可是其余线程仍是暂停,而并发指的是用户线程和GC线程同时执行。

ParNew收集器默认开启和CPU个数相同的线程数来进行回收,可使用参数:-XX:ParallelGCThreads来限制线程数
ParNew收集器工做原理以下图:
在这里插入图片描述
优势:在多CPU时,比Serial效率高。
缺点:收集过程暂停全部应用程序线程,单CPU时比Serial效率差
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,和ParNew同样也是一个并行的多线程收集器,Parallel Scanvenge收集器相比较于ParNew收集器,更关注与提高系统的吞吐量。

吞吐量指的是CPU用于运行用户代码的而时间于CPU总消耗时间的比值。
即:吞吐量=运行用户代码时间/(运行用户代码时间+GC时间)

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

`-XX:MaxGCPauseMillis//GC最大停顿毫秒数,必须大于0
-XX:GCTimeRatio//设置吞吐量大小,大于0小于100,默认值为99` 

*   1
*   2

咱们思考一个问题,假如咱们经过参数把容许最大停顿毫秒数设置的相对较小会怎么样?是否是GC速度就会变快了

答案是否认的。若是设置的时间太短,Parallel Scavenge收集器会牺牲吞吐量和新生代空间来交换。
好比新生代400Mb须要GC时间为100ms,而后手动设置为50ms,那么就会把新生代调小为200Mb,这样确定时间就降下来了,然而这种操做可能会下降吞吐量,假如说原先是10s触发一次GC,每次100ms,修改时间后编程5s触发一次GC,每次70ms,那么10s触发两次GC时间就变成了140ms,吞吐量反而下降。

若是不知道如何设置,那么还能够经过参数:-XX:+UseAdaptiveSizePolicy开启自适应策略(GC Ergonomics),这样咱们就不须要手动设置吞吐量和GC停顿时间了,虚拟机会根据运行状况手机监控信息来动态调整。

Paralled Old收集器

Paralled Old收集器是Parallel Scavenge收集器的老年代版本,可是这个收集器是jdk1.6以后才出现的,因此致使了在Paralled Old收集器出现以前Parallel Scavenge收集器一直找不到合适的“搭档”。由于Parallel Scavenge收集器没办法和CMS收集器配合使用(后面会介绍缘由),因此在Paralled Old收集器出现以前,若是新生代选择了Parallel Scavenge收集器,那么老年代就只能选择Serial Old收集器,而Serial Old收集器是单线程的,因此单单只是新生代替换成了多线程的吞吐量收集器Parallel Scavenge,在性能上并不必定有多少提高。

在注重吞吐量的业务系统中,能够考虑Parallel Scavenge+Paralled Old收集器配合使用,结合使用后的工做原理以下图所示:
在这里插入图片描述
PS:在jdk1.8中,默认收集器就是Parallel Scavenge+Parallel Old组合

CMS(Concurrent Mark Sweep)收集器

这是一种以实现GC时最短停顿时间为目标的收集器,也是一款真正实现了并发回收的收集器。固然,虽然是并发的,可是仍然须要Stop The World,只是尽量将这个时间缩到最短。

对于任何暂停时间要求较低的应用程序,都应该考虑使用此收集器。CMS收集器能够经过参数:-XX:+UseConcMarkSweepGC启用。

CMS收集器是基于算法标记-清除来实现的,整个过程分为4步:

  • 一、初始标记(inital mark)
    须要Stop The World。标记GC Roots对象,由于GC Root对象并不会不少,因此这个过程很是快。
  • 二、并发标记(concurrent mark)
    这个阶段能够和用户线程同时进行,也能够分为三步:
    (1)并发标记(CMS-concurrent-mark):主要是进行GC Roots Tracing。就是说根据第1步中找到的GC Root对象,开始搜索,这个过程相比阶段1是比较慢的。
    (2)预清理(CMS-concurrent-preclean),这个阶段是为了处理并发标记以后发生了变化的对象
    (3)可被终止的预清理(CMS-concurrent-abortable-preclean),这个预清理差很少,可是是能够被终止的,主要是分了尽量分担下面第3步的工做,这个阶段会有一个abort触发条件,该阶段存在的目的是但愿能发生一次Young GC,这样就能够减小Young区对象的数量,下降从新标记的工做量,由于从新标记会扫描整个堆内空间。能够经过参数-XX:+CMSScavengeBeforeRemark参数控制在从新标记前发生一次Young GC,默认为false。这个阶段发生的最大时间由-XX:CMSMaxAbortablePrecleanTime控制,默认5s
  • 三、从新标记(remark)
    须要Stop The World,这个阶段是为了修正在阶段2标记以后产生了变化的对象
  • 四、并发清除(concurrent sweep)
    和用户线程同时进行,开始正式清除垃圾,在此阶段也会产生垃圾,产生垃圾后没法清除,只能留待下一次GC。

CMS收集过程以下图所示:
在这里插入图片描述

CMS优缺点

  • 优势:并发收集、低停顿。
    其实最主要的是CMS把收集过程当中步骤拆分了,而最耗时的操做都是并发执行,天然就会低停顿了。
  • 缺点:产生大量空间碎片、并发阶段会下降吞吐量。
    CMS采用的是标记-清除算法,因此会产生大量的空间碎片。在阶段2和阶段4并发执行的时候,会占用CPU资源,就会致使应用程序变慢,下降了吞吐量。

Floating Garbage(浮动垃圾)

上面的步骤中,步骤2是并发标记,因此在标记过程当中,可能会有新的垃圾产生而没有被标记到。好比说对象A,刚扫描的时候是有效对象,而后继续扫描的时候,对象A又变成不可用了,而后还有并发清除的阶段,也可能会有新的垃圾产生,这种就称之为浮动垃圾(Floating Garbage)。CMS并不能收集浮动垃圾,只能等到下一次GC时再回收。

Concurrent Mode Failure(并发模式失败)

CMS收集器不能和其余收集器同样等到空间满了才开始触发GC,由于CMS收集的时候是并发的,并发的过程确定会持续产生对象,若是由于在垃圾收集期间内存不足而致使了GC失败,就称之为Concurrent Mode Failure。出现这种状况以后,Java虚拟机就会启动预备方案,启用Serial Old收集器替换CMS收集器,这时候整个GC过程都会Stop The World。

CMS收集器的触发阈值能够经过参数:-XX:CMSInitiatingOccupancyFraction=来进行设置,N为(0,100)之间,在jdk1.6中默认是92,即老年代空间使用率达到92%就会触发CMS收集器开始进行垃圾回收。

G1(Garbage-First)收集器

G1也是以实现GC时最短停顿时间为目标并发回收的收集器,它尝试以高几率知足垃圾收集(GC)暂停时间目标,同时实现高吞吐量。

在G1以前的其余收集器都是属于分代收集器,也就是说一个收集器要否则用于新生代,要否则就是用于老年代,而G1中,将堆的整个内存布局作了很大的修改,在G1中,将整个Java堆划分为多个大小相等的独立区域(Region),虽然在逻辑上还保留了新生代和老年代的概念,可是物理上已经没有隔离了。

G1收集器中堆内布局以下图所示:
在这里插入图片描述
上图中堆被划分为一组大小相同的Region,每一个Region都是连续的虚拟内存范围。
G1能够知道哪一个Region区域内大部分都是空的,这样就能够在每次容许的收集时间内去优先回收价值最大的Region区域(根据回收所得到的空间大小以及回收所须要的时间综合考虑),因此这也就是G1为何叫作Garbage-First的缘由。

PS:G1是JDK1.9的默认垃圾收集器

G1特色

通过上面的简单介绍,能够得出G1主要有如下特色:

  • 一、实现了并行与并发,尽量的缩短了Stop The World时间。
  • 二、分代收集:逻辑上依然保留了分代概念
  • 三、空间整合:总体来看是基于“标记-整理”算法来实现的(若是冲Region来看,是基于“复制”算法),因此不会产生大量内存空间碎片。
  • 四、支持可预测的停顿时间:能够经过参数来设置每次GC最大时间
  • 五、非实时收集:由于能够人为设置停顿时间,因此在指定时间范围内会进行优先选择收集,而不会收集全部被标记好的垃圾。

G1工做流程

G1收集器在工做流程上和CMS比较类似,只是在最后的步骤有所区别,主要通过了以下4个步骤:

  • 一、初始标记(Initial Marking):须要Stop The World。标记一下GC Roots可以关联的对象,而且修改TAMS(Next Top at Mark Start)的值,使得下一阶段并发运行时,能在正确可用的Region中建立对象。
  • 二、并发标记(Concurrent Marking):和CMS同样,主要是进行GC Roots Tracing,找出存活对象进行标记。
  • 三、最终标记(Final Marking):须要Stop The World。和CMS同样,这个阶段主要是为了修正在并发标记期间因用户程序继续运行而致使产生变更的对象。
  • 四、筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和成本进行排序,根据 用户所指望的GC停顿时间制定回收计划进行回收。

工做流程图以下所示:
在这里插入图片描述

G1应用场景

G1的第一个重点是为运行须要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小大约为6GB或更大,而且稳定且可预测的暂停时间低于0.5秒。

若是咱们的应用程序具备如下一个或多个特性,那么能够考虑切换到G1收集器。

  • 一、超过50%的Java堆被实时数据占用。
  • 二、对象分配率或提高率差别很大。
  • 三、当前应用程序GC停顿时间超过0.5到1秒,而又想缩短停顿时间的应用。

其余收集器

  • ZGC收集器:是Java11中提供的一种垃圾收集器。
  • Shenandoah:OpenJDK中包含的收集器,最开始是由RedHat公司开发,后来贡献给了OpenJDK。
  • Epsilon(A No-Op Garbage Collector):一款控制内存分配,可是不执行任何垃圾回收工做的收集器。一旦java的堆被耗尽,jvm就直接关闭。

如何选择垃圾回收器

垃圾收集器主要能够分为以下三大类:

  • 串行收集器:Serial和Serial Old
    只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。
  • 并行收集器[吞吐量优先]:Parallel Scanvenge和Parallel Old
    多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等若交互场景 。
  • 并发收集器[停顿时间优先]:CMS和G1。
    用户线程和垃圾收集线程同时执行(但并不必定是并行的,多是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。 适用于对时间有要求的场景,好比Web应用。

总结

本文主要介绍了肯定无效对象的两种算法,而且结合垃圾收集算法介绍了不一样类型的落地形式而产生的不一样垃圾收集器,本文将对比较偏向于理论,下一篇开始,JVM系列文章将会结合JVM系列前5篇文章来进一步结合实际场景以及相关监控工具的使用来进行实际场景分析。