关于java:JVM系列5深入分析Java垃圾收集算法和常用垃圾收集器

25次阅读

共计 8119 个字符,预计需要花费 21 分钟才能阅读完成。

前言

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

如何确定有效对象

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

援用计数法(Reference Counting)

这个算法很简略,效率也十分高。就是给每个对象增加一个援用计数器,每当有一个中央援用它时,计数器的值就加 1,当援用生效时,计数器的值就减 1,当计数器的值减为 0 时就表名这个对象不会再被应用,成为了无用对象,能够被回收。

这种算法尽管实现简略,效率也高,然而存在一个问题,咱们看上面一个场景:


上图中 4 个对象互相援用,然而并没有其余对象去援用他们,这种对象实际上也是有效对象,然而他们的援用计数器都是 1 而不是 0,所以援用计数法没方法解决这种“一坨垃圾”的场景。

可达性剖析算法(Reachability Analysis)

可达性剖析算法就是抉择一些对象作为起始点,这些对象称之为:GC Root。而后从 GC Root 开始向下搜寻,搜寻门路称之为援用链 (Reference Chain),当一个对象不在任何一条援用链上时,就阐明此对象是有效对象,能够被回收。
比如说上面这幅图,左边那一串相互援用的对象因为没有不在 GC Root 的援用链上,所以就是有效对象,可达性剖析算法无效的解决了相互援用对象无奈回收问题。

GC Root

在 Java 中,能够作为 GC Root 对象的包含上面几种:

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

留神:在剖析对象的过程中,为了确保后果的准确性,须要保障剖析过程中对象援用关系不会发生变化,而为了达到这个目标,就 须要暂停用户线程,这种操作也叫:Stop The World(STW)。

援用的分类

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

强援用(Strong Reference)

咱们写的代码中个别都是用的强援用,如:Object obj = new Object()这种就属于强援用,强援用只有还存在,肯定不会被回收,空间不够就间接抛出 OOM 异样

软援用(Soft Reference)

软援用是通过 SoftReference 类来实现的。软援用能够用来示意一些还有用但又是非必须的对象,零碎在行将溢出之前,如果发现有软援用的对象存在,会对其进行二次回收,回收之后内存还是不够,就会抛出 OOM 异样。

弱援用(Weak Reference)

弱援用是通过 WeakReference 类来实现的。弱援用也是用来示意非必须对象的,然而相比拟软援用,弱援用的对象会在第一次垃圾回收的时候就被回收掉。

虚援用(Phantom Reference)

虚援用是通过 PhantomReference 类来实现的,也被成为幽灵援用或者幻影援用。这是最弱的一种援用关系。一个对象是否有虚援用的存在,齐全不对其生存工夫形成影响。也无奈通过虚援用来获得一个对象实例。设置为虚援用的惟一用途可能就是当这个对象被回收的时候能够收到一个零碎告诉。

垃圾收集算法

下面剖析了如何确定一个对象属于可回收对象的两种算法,那么当一个对象被确定为垃圾之后,就须要对其进行回收,回收也有不同的算法,上面就来看一下罕用的垃圾收集算法

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

标记 - 革除算法次要分为两步,标记 (Mark) 和革除 (Sweep)。
比如说有上面一块内存区域(红色 - 未应用,灰色 - 无援用,蓝色 - 有援用):

而后标记 - 革除算法会进行如下两个步骤:

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

标记革除之后失去如下图所示:

能够很显著看到,回收之后内存空间是不间断的,产生了大量的内存空间碎片。过多内存碎片最间接的就是能够导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

标记 - 革除算法的毛病

1、标记和革除两个过程都比拟耗时,效率不高
2、会产生大量不间断的内存碎片。

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

复制 (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 步:

  • 1、初始标记 (inital mark)
    须要 Stop The World。标记 GC Roots 对象,因为 GC Root 对象并不会很多,所以这个过程十分快。
  • 2、并发标记 (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
  • 3、从新标记 (remark)
    须要 Stop The World,这个阶段是为了修改在阶段 2 标记之后产生了变动的对象
  • 4、并发革除 (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 次要有以下特点:

  • 1、实现了并行与并发,尽可能的缩短了 Stop The World 工夫。
  • 2、分代收集:逻辑上仍然保留了分代概念
  • 3、空间整合:整体来看是基于“标记 - 整顿”算法来实现的(如果冲 Region 来看,是基于“复制”算法),所以不会产生大量内存空间碎片。
  • 4、反对可预测的进展工夫:能够通过参数来设置每次 GC 最大工夫
  • 5、非实时收集:因为能够人为设置进展工夫,所以在指定工夫范畴内会进行优先选择收集,而不会收集所有被标记好的垃圾。

G1 工作流程

G1 收集器在工作流程上和 CMS 比拟类似,只是在最初的步骤有所区别,次要通过了如下 4 个步骤:

  • 1、初始标记 (Initial Marking):须要 Stop The World。标记一下 GC Roots 可能关联的对象,并且批改 TAMS(Next Top at Mark Start) 的值,使得下一阶段并发运行时,能在正确可用的 Region 中创建对象。
  • 2、并发标记(Concurrent Marking):和 CMS 一样,次要是进行 GC Roots Tracing,找出存活对象进行标记。
  • 3、最终标记(Final Marking):须要 Stop The World。和 CMS 一样,这个阶段次要是为了修改在并发标记期间因用户程序持续运行而导致产生变动的对象。
  • 4、筛选回收(Live Data Counting and Evacuation):对各个 Region 的回收价值和老本进行排序,依据 用户所冀望的 GC 进展工夫制订回收打算进行回收。

工作流程图如下所示:

G1 利用场景

G1 的第一个重点是为运行须要大堆且 GC 提早无限的应用程序的用户提供解决方案。这意味着堆大小大概为 6GB 或更大,并且稳固且可预测的暂停工夫低于 0.5 秒。

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

  • 1、超过 50% 的 Java 堆被实时数据占用。
  • 2、对象分配率或晋升率差别很大。
  • 3、以后应用程序 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 篇文章来进一步结合实际场景以及相干监控工具的应用来进行理论场景剖析。

正文完
 0