关于java:万字长文深入理解JVM垃圾收集机制

34次阅读

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

垃圾收集算法

标记 - 革除算法

最根底的收集算法是“标记 - 革除”(Mark-Sweep)算法,分两个阶段:首先标记出所有须要回收的对象,在标记实现后对立回收所有被标记的对象。

有余:一个是效率问题,标记和革除两个过程的效率都不高;另一个是空间问题,标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能导致当前在程序运行过程须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一个的垃圾收集动作。

复制算法

为了解决效率问题,一种称为复制 (Copying) 的收集算法呈现了,它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上,而后再把曾经应用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。代价是内存放大为原来的一半。

商业虚拟机用这个回收算法来回收新生代。IBM 钻研表明 98% 的对象是“朝生夕死“,不须要依照 1 - 1 的比例来划分内存空间,而是将内存分为一块较大的”Eden“空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一个 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的比例是 8 -1. 即每次可用整个新生代的 90%, 只有一个 survivor,即 1 /10 被”节约“。当然,98% 的对象回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够时,须要依赖其余内存 (老年代) 进行调配担保(Handle Promotion).

如果另外一块 survivor 空间没有足够空间寄存上一次新生代收集下来的存活对象时,这些对象将间接通过调配担保机制进入老年代。

eden survivor 复制过程概述

Eden Space 字面意思是伊甸园,对象被创立的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的 survivor 区域。

Survivor Space 幸存者区,用于保留在 eden space 内存区域中通过垃圾回收后没有被回收的对象。Survivor 有两个,别离为 To Survivor、From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候 Eden 区域不能被回收的对象被放入到空的 survivor(也就是 To Survivor,同时 Eden 区域的内存会在垃圾回收的过程中全副开释),另一个 survivor(即 From Survivor)里不能被回收的对象也会被放入这个 survivor(即 To Survivor),而后 To Survivor 和 From Survivor 的标记会调换,始终保障一个 survivor 是空的。

为啥须要两个 survivor?因为须要一个残缺的空间来复制过去。当满的时候降职。每次都往标记为 to 的外面放,而后调换,这时 from 曾经被清空,能够当作 to 了。

标记 - 整顿算法

复制收集算法在对象成活率较高时就要进行较多的复制操作,效率将会变低。更要害的是,如果不想节约 50% 的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都 100% 存活的极其状况,所以,老年代个别不能间接选用这种算法。

依据老年代的特点,有人提出一种”标记 - 整顿“Mark-Compact 算法,标记过程依然和标记 - 革除一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理端边界以外的内存.

分代收集算法

以后商业虚拟机的垃圾收集都采纳”分代收集“(Generational Collection)算法,这种算法依据对象存活周期的不同将内存划分为几块。个别把 Java 堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。在新生代,每次垃圾收集时都发现少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率较高,没有额定的空间对它进行调配担保,就必须应用”标记 - 清理“和”标记 - 整顿“算法来进行回收。

HotSpot 算法实现

在 Java 语言中,可作为 GC Roots 的对象包含上面几种:

  • 虚拟机栈 (栈帧中的本地变量表) 中援用的对象
  • 办法去中类动态属性援用的对象
  • 办法区中常量援用的对象
  • 本地办法栈中 JNI(即个别说的 Native 办法)援用的对象

从可达性剖析中从 GC Roots 节点找援用链这个操作为例,可作为 GC Roots 的节点次要在全局性的援用 (例如常量或类动态属性) 与执行上下文 (例如栈帧中的本地变量表) 中,当初很多利用仅仅办法区就有数百兆,如果要一一查看外面的援用,必然耗费很多工夫。

可达性剖析对执行工夫的敏感还体现在 GC 进展上,因为这项剖析工作必须在一个能确保一致性的快照中进行 – 这里”一致性“的意思是指整个剖析期间整个执行零碎看起来就像被解冻在某个工夫点,不能够呈现剖析过程中对象援用关系还在一直变动的状况,该点不满足的话剖析后果准确性就无奈失去保障。这点是导致 GC 进行时必须进展所有 Java 执行线程 (Sun 公司将这件事件称为”Stop The World“) 的一个重要起因,即便是在号称 (简直) 不会产生进展的 CMS 收集器中,枚举根节点时也必须进展的。

平安点,Safepoint

垃圾收集器

Serial 收集器

标记 - 复制。

单线程,一个 CPU 或一条收集线程去实现垃圾收集工作,收集时必须暂停其余所有的工作线程,直到它完结。

尽管如此,它仍然是虚拟机运行在 Client 模式下的默认 新生代 收集器。简略而高效。

ParNew 收集器

ParNew 是 Serial 收集器的多线程版本。Server 模式下默认 新生代 收集器,除了 Serial 收集器之外,只有它能与 CMS 收集器配合工作。

并行 Parallel

指多条垃圾收集线程并行工作,但此时用户线程依然处于期待状态。

并发 Concurrent

指用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),用户程序再持续运行,而垃圾收集程序运行于另一个 CPU 上。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个 新生代 收集器,它也是应用复制算法的收集器。看上去来 ParNew 一样,有什么特地?

Parallel Scavenge 收集器的特点是它的关注点与其余收集器不同,CMS 等收集器关注点是尽可能缩短垃圾收集时用户线程的进展工夫。而 Parallel Scavenge 收集器的指标则是达到一个可管制的吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的工夫和 CPU 总小号工夫的比值,即吞吐量 = 运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫),虚拟机总共运行了 100min,其中垃圾收集破费了 1min,那吞吐量就是 99%.

进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度能晋升用户体验,而高吞吐量则能够高效地利用 CPU 工夫,次要适宜在后盾运算而不须要太多交互的工作。

Parallel Scavenge 收集器提供了两个参数用于准确管制吞吐量,别离是管制最大垃圾收集进展工夫 -XX:MaxGCPauseMillis以及间接设置吞吐量大小的-XX:GCTimeRatio

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器。给 Client 模式下的虚拟机应用。

新生代采纳复制算法,暂停所有用户线程;

老年代采纳标记 - 整顿算法,暂停所有用户线程;

Parallel Old 收集器

这里留神,Parallel Scavage 收集器架构中自身有 PS MarkSweep 收集器来收集老年代,并非间接应用了 Serial Old, 但二者靠近。自己 win10 64 位零碎,jdk1.8.0_102,测试默认垃圾收集器为:PS MarkSweep 和 PS Scavenge。也就是说 Java8 的默认并不是 G1。

这是”吞吐量优先“,重视吞吐量以及 CPU 资源敏感的场合都能够优先思考 Parallel Scavenge 和 Parallel Old(PS Mark Sweep)。Java8 默认就是这个。

CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收进展工夫为指标的收集器。目前很大一部分的 Java 利用集中在互联网站或者 B / S 零碎的服务端上,这类尤其器重服务的响应速度,心愿零碎进展工夫最短。CMS 收集器就十分合乎这类利用的需要。

CMS 基于 标记 - 革除 算法实现。整个过程分为 4 个步骤:

  1. 初始标记(CMS initial mark) -stop the world
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark) -stop the world
  4. 并发革除(CMS concurrent sweep)

初始标记,从新标记这两个步骤依然须要 Stop The World, 初始标记仅仅标记以下 GC Roots 能间接关联的对象,速度很快。

并发标记就是进行 GC Roots Tracing 的过程;

而从新标记阶段则是为了修改并发标记期间因为用户程序持续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段进展比初始标记略微长,但远比并发标记的工夫短。

整个过程耗时最长的并发标记和并发革除过程,收集器都能够与用户线程一起工作。总体上来说,CMS 收集器的内存回收过程与用户线程一起并发执行的。

CMS 特点:并发收集,低进展。

毛病

1.CMS 收集器对 CPU 资源十分敏感。默认启动的回收线程数是(CPU+3)/4. 当 CPU 4 个以上时,并发回收垃圾收集线程不少于 25% 的 CPU 资源。

2.CMS 收集器无奈解决浮动垃圾(Floating Garbage), 可能呈现”Concurrent Mode Failure“失败而导致另一次 Full GC 的产生。因为 CMS 并发清理时,用户线程还在运行,随同产生新垃圾,而这一部分呈现在标记之后,只能下次 GC 时再清理。这一部分垃圾就称为”浮动垃圾“。

因为 CMS 运行时还须要给用户空间持续运行,则不能等老年代简直被填满再进行收集,须要预留一部分空间提供并发收集时,用户程序运行。JDK1.6 中,CMS 启动阈值为 92%. 若预留内存不够用户应用,则呈现一次 Concurent Mode Failure 失败。这时虚拟机启动后备预案,长期启用 Serial Old 收集老年代,这样进展工夫很长。

3.CMS 基于”标记 - 革除“算法实现的,则会产生大量空间碎片,空间碎片过多时,没有间断空间调配给大对象,不得不提前触发一次 FUll GC。当然能够开启 -XX:+UseCMSCompactAtFullCollection(默认开),在 CMS 顶不住要 FullGC 时开启内存碎片合并整顿过程。内存整理过程是无奈并发的,空间碎片问题没了,但进展工夫变长。

面试题:CMS 一共会有几次 STW

首先,答复两次,初始标记和从新标记须要。

而后,CMS 并发的代价是预留空间给用户,预留有余的时候触发 FUllGC,这时 Serail Old 会 STW.

而后,CMS 是标记 - 革除算法,导致空间碎片,则没有间断空间调配大对象时,FUllGC, 而 FUllGC 会开始碎片整顿,STW.

即 2 次或屡次。

CMS 什么时候 FUll GC

除间接调用 System.gc 外,触发 Full GC 执行的状况有如下四种。

1. 旧生代空间有余

旧生代空间只有在新生代对象转入及创立为大对象、大数组时才会呈现有余的景象,当执行 Full GC 后空间依然有余,则抛出如下谬误:
java.lang.OutOfMemoryError: Java heap space
为防止以上两种情况引起的 FullGC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创立过大的对象及数组。

2. Permanet Generation 空间满

PermanetGeneration 中寄存的为一些 class 的信息等,当零碎中要加载的类、反射的类和调用的办法较多时,Permanet Generation 可能会被占满,在未配置为采纳 CMS GC 的状况下会执行 Full GC。如果通过 Full GC 依然回收不了,那么 JVM 会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为防止 Perm Gen 占满造成 Full GC 景象,可采纳的办法为增大 Perm Gen 空间或转为应用 CMS GC。

3. CMS GC 时呈现 promotion failed 和 concurrent mode failure

对于采纳 CMS 进行旧生代 GC 的程序而言,尤其要留神 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种情况,当这两种情况呈现时可能会触发 Full GC。
promotionfailed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入旧生代,而此时旧生代空间有余造成的。
应答措施为:增大 survivorspace、旧生代空间或调低触发并发 GC 的比率,但在 JDK 5.0+、6.0+ 的版本中有可能会因为 JDK 的 bug29 导致 CMS 在 remark 结束后很久才触发 sweeping 动作。对于这种情况,可通过设置 -XX:CMSMaxAbortablePrecleanTime=5(单位为 ms)来防止。

4. 统计失去的 Minor GC 降职到旧生代的均匀大小大于旧生代的残余空间

这是一个较为简单的触发状况,Hotspot 为了防止因为新生代对象降职到旧生代导致旧生代空间有余的景象,在进行 Minor GC 时,做了一个判断,如果之前统计所失去的 Minor GC 降职到旧生代的均匀大小大于旧生代的残余空间,那么就间接触发 Full GC。
例如程序第一次触发 MinorGC 后,有 6MB 的对象降职到旧生代,那么当下一次 Minor GC 产生时,首先查看旧生代的残余空间是否大于 6MB,如果小于 6MB,则执行 Full GC。
当新生代采纳 PSGC 时,形式稍有不同,PS GC 是在 Minor GC 后也会查看,例如下面的例子中第一次 Minor GC 后,PS GC 会查看此时旧生代的残余空间是否大于 6MB,如小于,则触发对旧生代的回收。
除了以上 4 种情况外,对于应用 RMI 来进行 RPC 或治理的 Sun JDK 利用而言,默认状况下会一小时执行一次 Full GC。可通过在启动时通过 - java-Dsun.rmi.dgc.client.gcInterval=3600000 来设置 Full GC 执行的间隔时间或通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc。

G1

什么是垃圾回收

首先,在理解 G1 之前,咱们须要分明的晓得,垃圾回收是什么?简略的说垃圾回收就是回收内存中不再应用的对象。

垃圾回收的根本步骤

回收的步骤有 2 步:

1. 查找内存中不再应用的对象

2. 开释这些对象占用的内存

1, 查找内存中不再应用的对象

那么问题来了,如何判断哪些对象不再被应用呢?咱们也有 2 个办法:

1. 援用计数法
援用计数法就是如果一个对象没有被任何援用指向,则可视之为垃圾。这种办法的毛病就是不能检测到环的存在。

2. 根搜索算法

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到 GC Roots 没有任何援用链相连时,则证实此对象是不可用的。

当初咱们曾经晓得如何找出垃圾对象了,如何把这些对象清理掉呢?

2. 开释这些对象占用的内存

常见的形式有复制或者间接清理,然而间接清理会存在内存碎片,于是就会产生了清理再压缩的形式。

总得来说就产生了三种类型的回收算法。

1. 标记 - 复制

2. 标记 - 清理

3. 标记 - 整顿

基于分代的假如

因为对象的存活工夫有长有短,所以对于存活工夫长的对象,缩小被 gc 的次数能够防止不必要的开销。这样咱们就把内存分成新生代和老年代,新生代寄存刚创立的和存活工夫比拟短的对象,老年代寄存存活工夫比拟长的对象。这样每次仅仅清理年老代,老年代仅在必要时时再做清理能够极大的进步 GC 效率,节俭 GC 工夫。

Java 垃圾收集器的历史

第一阶段,Serial(串行)收集器

在 jdk1.3.1 之前,java 虚拟机仅仅能应用 Serial 收集器。Serial 收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是阐明它只会应用一个 CPU 或一条收集线程去实现垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其余所有的工作线程,直到它收集完结。

PS:开启 Serial 收集器的形式

-XX:+UseSerialGC

第二阶段,Parallel(并行)收集器

Parallel 收集器也称吞吐量收集器,相比 Serial 收集器,Parallel 最次要的劣势在于应用多线程去实现垃圾清理工作,这样能够充分利用多核的个性,大幅升高 gc 工夫。

PS: 开启 Parallel 收集器的形式

-XX:+UseParallelGC -XX:+UseParallelOldGC

第三阶段,CMS(并发)收集器

CMS 收集器在 Minor GC 时会暂停所有的利用线程,并以多线程的形式进行垃圾回收。在 Full GC 时不再暂停利用线程,而是应用若干个后盾线程定期的对老年代空间进行扫描,及时回收其中不再应用的对象。

PS: 开启 CMS 收集器的形式

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

第四阶段,G1(并发)收集器

G1 收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短解决超大堆(大于 4GB)时产生的进展。绝对于 CMS 的劣势而言是内存碎片的产生率大大降低。

PS: 开启 G1 收集器的形式

-XX:+UseG1GC

理解 G1

G1 的第一篇 paper(附录 1)发表于 2004 年,在 2012 年才在 jdk1.7u4 中可用。oracle 官网打算在 jdk9 中将 G1 变成默认的垃圾收集器,以代替 CMS。为何 oracle 要极力推荐 G1 呢,G1 有哪些长处

首先,G1 的设计准则就是简略可行的性能调优

开发人员仅仅须要申明以下参数即可:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中 -XX:+UseG1GC 为开启 G1 垃圾收集器,-Xmx32g 设计堆内存的最大内存为 32G,-XX:MaxGCPauseMillis=200 设置 GC 的最大暂停工夫为 200ms。如果咱们须要调优,在内存大小肯定的状况下,咱们只须要批改最大暂停工夫即可。

其次,G1 将新生代,老年代的物理空间划分勾销了。

这样咱们再也不必独自的空间对每个代进行设置了,不必放心每个代内存是否足够。

取而代之的是,G1 算法将堆划分为若干个区域(Region),它依然属于分代收集器。不过,这些区域的一部分蕴含新生代,新生代的垃圾收集仍然采纳暂停所有利用线程的形式,将存活对象拷贝到老年代或者 Survivor 空间。老年代也分成很多区域,G1 收集器通过将对象从一个区域复制到另外一个区域,实现了清理工作。这就意味着,在失常的处理过程中,G1 实现了堆的压缩(至多是局部堆的压缩),这样也就不会有 cms 内存碎片问题的存在了。

在 G1 中,还有一种非凡的区域,叫 Humongous 区域。如果一个对象占用的空间超过了分区容量 50% 以上,G1 收集器就认为这是一个巨型对象。这些巨型对象,默认间接会被调配在年轻代,然而如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门寄存巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找间断的 H 分区来存储。为了能找到间断的 H 区,有时候不得不启动 Full GC。

PS:在 java 8 中,长久代也挪动到了一般的堆内存空间中,改为元空间。

对象调配策略

说起大对象的调配,咱们不得不谈谈对象的调配策略。它分为 3 个阶段:

1.TLAB(Thread Local Allocation Buffer)线程本地调配缓冲区
2.Eden 区中调配
3.Humongous 区调配

TLAB 为线程本地调配缓冲区,它的目标为了使对象尽可能快的调配进去。如果对象在一个共享的空间中调配,咱们须要采纳一些同步机制来治理这些空间内的闲暇空间指针。在 Eden 空间中,每一个线程都有一个固定的分区用于调配对象,即一个 TLAB。调配对象时,线程之间不再须要进行任何的同步。

对 TLAB 空间中无奈调配的对象,JVM 会尝试在 Eden 空间中进行调配。如果 Eden 空间无奈包容该对象,就只能在老年代中进行调配空间。

最初,G1 提供了两种 GC 模式,Young GC 和 Mixed GC,两种都是 Stop The World(STW)的。上面咱们将别离介绍一下这 2 种模式。

G1 Young GC

Young GC 次要是对 Eden 区进行 GC,它在 Eden 空间耗尽时会被触发。在这种状况下,Eden 空间的数据挪动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的局部数据会间接降职到年轻代空间。Survivor 区的数据挪动到新的 Survivor 区中,也有局部数据降职到老年代空间中。最终 Eden 空间的数据为空,GC 进行工作,利用线程继续执行。

这时,咱们须要思考一个问题,如果仅仅 GC 新生代对象,咱们如何找到所有的根对象呢?老年代的所有对象都是根么?那这样扫描下来会消耗大量的工夫。于是,G1 引进了 RSet 的概念。它的全称是 Remembered Set,作用是跟踪指向某个 heap 区内的对象援用。

在 CMS 中,也有 RSet 的概念,在老年代中有一块区域用来记录指向新生代的援用。这是一种 point-out,在进行 Young GC 时,扫描根时,仅仅须要扫描这一块区域,而不须要扫描整个老年代。

但在 G1 中,并没有应用 point-out,这是因为一个分区太小,分区数量太多,如果是用 point-out 的话,会造成大量的扫描节约,有些基本不须要 GC 的分区援用也扫描了。于是 G1 中应用 point-in 来解决。point-in 的意思是哪些分区援用了以后分区中的对象。这样,仅仅将这些对象当做根来扫描就防止了有效的扫描。因为新生代有多个,那么咱们须要在新生代之间记录援用吗?这是不必要的,起因在于每次 GC 时,所有新生代都会被扫描,所以只须要记录老年代到新生代之间的援用即可。

须要留神的是,如果援用的对象很多,赋值器须要对每个援用做解决,赋值器开销会很大,为了解决赋值器开销这个问题,在 G1 中又引入了另外一个概念,卡表(Card Table)。一个 Card Table 将一个分区在逻辑上划分为固定大小的间断区域,每个区域称之为卡。卡通常较小,介于 128 到 512 字节之间。Card Table 通常为字节数组,由 Card 的索引(即数组下标)来标识每个分区的空间地址。默认状况下,每个卡都未被援用。当一个地址空间被援用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被援用,此外 RSet 也将这个数组下标记录下来。个别状况下,这个 RSet 其实是一个 Hash Table,Key 是别的 Region 的起始地址,Value 是一个汇合,外面的元素是 Card Table 的 Index。

Young GC 阶段

阶段 1:根扫描

动态和本地对象被扫描

阶段 2:更新 RS

解决 dirty card 队列更新 RS

阶段 3:解决 RS

检测从年老代指向年轻代的对象

阶段 4:对象拷贝

拷贝存活的对象到 survivor/old 区域

阶段 5:解决援用队列

软援用,弱援用,虚援用解决

G1 Mix GC

Mix GC 不仅进行失常的新生代垃圾收集,同时也回收局部后盾扫描线程标记的老年代分区。

它的 GC 步骤分 2 步:

1. 全局并发标记(global concurrent marking)
2. 拷贝存活对象(evacuation)

在进行 Mix GC 之前,会先进行 global concurrent marking(全局并发标记)。global concurrent marking 的执行过程是怎么的呢?

在 G1 GC 中,它次要是为 Mixed GC 提供标记服务的,并不是一次 GC 过程的一个必须环节。global concurrent marking 的执行过程分为五个步骤:

初始标记(initial mark,STW)

在此阶段,G1 GC 对根进行标记。该阶段与惯例的 (STW) 年老代垃圾回收密切相关。

根区域扫描(root region scan

G1 GC 在初始标记的存活区扫描对老年代的援用,并标记被援用的对象。该阶段与应用程序(非 STW)同时运行,并且只有实现该阶段后,能力开始下一次 STW 年老代垃圾回收。

并发标记(Concurrent Marking)

G1 GC 在整个堆中查找可拜访的(存活的)对象。该阶段与应用程序同时运行,能够被 STW 年老代垃圾回收中断

最终标记(Remark,STW)

该阶段是 STW 回收,帮忙实现标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被拜访的存活对象,并执行援用解决。

革除垃圾(Cleanup,STW)

在这个最初阶段,G1 GC 执行统计和 RSet 污染的 STW 操作。在统计期间,G1 GC 会辨认齐全闲暇的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到闲暇列表时为局部并发。

三色标记算法

提到并发标记,咱们不得不理解并发标记的三色标记算法。它是形容追踪式回收器的一种有用的办法,利用它能够推演回收器的正确性。首先,咱们将对象分成三种类型的。

彩色: 根对象,或者该对象与它的子对象都被扫描

灰色: 对象自身被扫描, 但还没扫描完该对象中的子对象

红色: 未被扫描对象,扫描实现所有对象之后,最终为红色的为不可达对象,即垃圾对象

当 GC 开始扫描对象时,依照如下图步骤进行对象的扫描:

根对象被置为彩色,子对象被置为灰色。

持续由灰色遍历, 将已扫描了子对象的对象置为彩色。

遍历了所有可达的对象后,所有可达的对象都变成了彩色。不可达的对象即为红色,须要被清理。

这看起来很美妙,然而如果在标记过程中,应用程序也在运行,那么对象的指针就有可能扭转。这样的话,咱们就会遇到一个问题:对象失落问题

咱们看上面一种状况,当垃圾收集器扫描到上面状况时:

这时候应用程序执行了以下操作:

A.c=C
B.c=null

这样,对象的状态图变成如下情景:

这时候垃圾收集器再标记扫描的时候就会下图成这样:

很显然,此时 C 是红色,被认为是垃圾须要清理掉,显然这是不合理的。那么咱们如何保障应用程序在运行的时候,GC 标记的对象不失落呢?有如下 2 中可行的形式:

1. 在插入的时候记录对象
2. 在删除的时候记录对象

刚好这对应 CMS 和 G1 的 2 种不同实现形式:

在 CMS 采纳的是增量更新(Incremental update),只有在写屏障(write barrier)里发现要有一个白对象的援用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在 G1 中,应用的是 STAB(snapshot-at-the-beginning)的形式,删除的时候记录所有的对象,它有 3 个步骤:

1. 在开始标记的时候生成一个快照图标记存活对象

2. 在并发标记的时候所有被扭转的对象入队(在 write barrier 里把所有旧的援用所指向的对象都变成非白的)

3. 可能存在游离的垃圾,将在下次被收集

这样,G1 到当初能够晓得哪些老的分区可回收垃圾最多。当全局并发标记实现后,在某个时刻,就开始了 Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行失常的新生代垃圾收集,同时也回收局部后盾扫描线程标记的分区。混合式垃圾收集如下图:

混合式 GC 也是采纳的复制的清理策略,当 GC 实现后,会从新开释空间。

调优实际

MaxGCPauseMillis调优

后面介绍过应用 GC 的最根本的参数:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

后面 2 个参数都好了解,前面这个 MaxGCPauseMillis 参数该怎么配置呢?这个参数从字面的意思上看,就是容许的 GC 最大的暂停工夫。G1 尽量确保每次 GC 暂停的工夫都在设置的 MaxGCPauseMillis 范畴内。那 G1 是如何做到最大暂停工夫的呢?这波及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域汇合。

Young GC:选定所有新生代里的 region。通过管制新生代的 region 个数来管制 young GC 的开销。

Mixed GC:选定所有新生代里的 region,外加依据 global concurrent marking 统计得出收集收益高的若干老年代 region。在用户指定的开销指标范畴内尽可能抉择收益高的老年代 region。

在了解了这些后,咱们再设置最大暂停工夫就好办了。首先,咱们能容忍的最大暂停工夫是有一个限度的,咱们须要在这个限度范畴内设置。然而应该设置的值是多少呢?咱们须要在吞吐量跟 MaxGCPauseMillis 之间做一个均衡。如果 MaxGCPauseMillis 设置的过小,那么 GC 就会频繁,吞吐量就会降落。如果 MaxGCPauseMillis 设置的过大,应用程序暂停工夫就会变长。G1 的默认暂停工夫是 200 毫秒,咱们能够从这里动手,调整适合的工夫。

其余调优参数

-XX:G1HeapRegionSize=n

设置的 G1 区域的大小。值是 2 的幂,范畴是 1 MB 到 32 MB 之间。指标是依据最小的 Java 堆大小划分出约 2048 个区域。

-XX:ParallelGCThreads=n

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量雷同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这实用于大多数状况,除非是较大的 SPARC 零碎,其中 n 的值能够是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45 

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

防止应用以下参数:

防止应用 -Xmn 选项或 -XX:NewRatio 等其余相干选项显式设置年老代大小。固定年老代的大小会笼罩暂停工夫指标。

触发 Full GC

在某些状况下,G1 触发了 Full GC,这时 G1 会进化应用 Serial 收集器来实现垃圾的清理工作,它仅仅应用单线程来实现 GC 工作,GC 暂停工夫将达到秒级别的。整个利用处于假死状态,不能解决任何申请,咱们的程序当然不心愿看到这些。那么产生 Full GC 的状况有哪些呢?

并发模式失败

G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。这种情景下,须要减少堆大小,或者调整周期(例如减少线程数 -XX:ConcGCThreads 等)。

降职失败或者疏散失败

G1 在进行 GC 的时候没有足够的内存供存活对象或降职对象应用,由此触发了 Full GC。能够在日志中看到 (to-space exhausted) 或者(to-space overflow)。解决这种问题的形式是:

a. 减少 -XX:G1ReservePercent 选项的值(并相应减少总的堆大小),为“指标空间”减少预留内存量。

b. 通过缩小-XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

c. 也能够通过减少 -XX:ConcGCThreads 选项的值来减少并行标记线程的数目。

巨型对象调配失败

当巨型对象找不到适合的空间进行调配时,就会启动 Full GC,来开释空间。这种状况下,应该防止调配大量的巨型对象,减少内存或者增大 -XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

正文完
 0