关于垃圾回收:九神带你入门JVM下

47次阅读

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

咱们接着下面一篇持续学习 JVM 的基本知识。

对象存活判断

上篇中咱们介绍过 JVM 垃圾回收综述中说过一次垃圾回收之后会有一些对象存活。这节咱们介绍两个判断对象存活的算法。

判断对象存活有援用计数算法和可达性剖析算法。

1、援用计数算法

给每一个对象增加一个援用计数器,每当有一个中央援用它时,计数器值加 1;每当有一个中央不再援用它时,计数器值减 1,这样只有计数器的值不为 0,就阐明还有中央援用它,它就不是无用的对象。

这种办法看起来非常简单,但目前许多支流的虚拟机都没有选用这种算法来治理内存,起因就是当某些对象之间相互援用时,无奈判断出这些对象是否已死。如下图,对象 1 和对象 2 都没有被堆外的变量援用,而是被对方相互援用,这时他们尽管没有用途了,然而援用计数器的值依然是 1,无奈判断他们是死对象,垃圾回收器也就无奈回收。

2、可达性剖析算法

理解可达性剖析算法之前先理解一个概念——GC Roots,垃圾收集的终点,能够作为 GC Roots 的有虚拟机栈中本地变量表中援用的对象、办法区中动态属性援用的对象、办法区中常量援用的对象、本地办法栈中 JNI(Native 办法)援用的对象。当一个对象到 GC Roots 没有任何援用链相连(GC Roots 到这个对象不可达)时,就阐明此对象是不可用的,是死对象。如下图:object1、object2、object3、object4 和 GC Roots 之间有可达门路,这些对象不会被回收,但 object5、object6、object7 到 GC Roots 之间没有可达门路,这些对象就是死对象。

下面被断定为非存活的死对象(object5、object6、object7)并不是必死无疑,还有解救的余地。进行可达性剖析后对象和 GC Roots 之间没有援用链相连时,对象将会被进行一次标记,接着会判断如果对象没有笼罩 Object 的 finalize()办法或者 finalize()办法曾经被虚拟机调用过,那么它们就会革除;如果对象笼罩了 finalize()办法且还没有被调用,则会执行 finalize()办法中的内容,所以在 finalize()办法中如果从新与 GC Roots 援用链上的对象关联就能够援救本人。当然,理论中个别不会这么做。

GC 算法

接下来讲 GC 的算法,次要有标记 - 革除算法、复制算法、标记 - 整顿算法、分代收集算法。

1、标记 - 革除算法

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

长处:不须要进行对象的挪动,并且仅对不存活的对象进行解决,在存活对象比拟多的状况极为无效。

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

上面两张图从两个角度说明了标记 - 分明算法:

2、复制算法

为了解决效率问题,一种称为复制 (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 了。

3、标记 - 整顿算法

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

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

上面两张图讲了这个算法的过程:

4、分代收集算法

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

这种算法就是咱们在后面 JVM 垃圾回收综述中讲述的内容。其本质是更为灵便的应用”标记 - 清理“和”标记 - 整顿“算法。

常见的 GC 回收器

当初常见的垃圾收集器有如下几种

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、CMS、Parallel Old

堆内存垃圾收集器:G1

如图所示:

0、垃圾收集工夫

当程序运行时,各种数据、对象、线程、内存等都时刻在发生变化,当下达垃圾收集命令后垃圾收集器并不会立即执行垃圾收集。为了搞明确垃圾收集器的工作原理,咱们须要讲两个名词:平安点 (safepoint) 和安全区(safe region)。

平安点:从线程角度看,平安点能够了解为是在代码执行过程中的一些非凡地位,当线程执行到平安点的时候,阐明虚拟机以后的状态是平安的,如果有须要,能够在这里暂停用户线程。当垃圾收集时,如果须要暂停以后的用户线程,但用户线程过后没在平安点上,则应该期待这些线程执行到平安点再暂停。

安全区:平安点是绝对于运行中的线程来说的,对于如 sleep 或 blocked 等状态的线程,收集器不会期待这些线程被调配 CPU 工夫,这时候只有线程处于安全区中,就能够算是平安的。安全区就是在一段代码片段中,援用关系不会发生变化,能够看作是被扩大、拉长了的平安点。

GC 过程肯定会产生STW(Stop The World),而一旦产生 STW 必然会影响用户应用,所以 GC 的倒退都是在围绕缩小 STW 工夫这一目标。

1、Serial 收集器

Serial 是一款用于新生代的单线程收集器,采纳复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。如下是 Serial 收集器和 Serial Old 收集器联合进行垃圾收集的示意图,当用户线程都执行到平安点时,所有线程暂停执行,Serial 收集器以单线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行。

实用场景:Client 模式(桌面利用);单核服务器。能够用 -XX:+UserSerialGC 来抉择 Serial 作为新生代收集器。

2、ParNew 收集器

ParNew 就是一个 Serial 的多线程版本,其它与 Serial 并无区别。ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的成果,它默认开启的收集线程数和 CPU 数量统一,能够通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。如下是 ParNew 收集器和 Serial Old 收集器联合进行垃圾收集的示意图,当用户线程都执行到平安点时,所有线程暂停执行,ParNew 收集器以多线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行。

实用场景:多核服务器;与 CMS 收集器搭配应用。当应用 -XX:+UserConcMarkSweepGC 来抉择 CMS 作为老年代收集器时,新生代收集器默认就是 ParNew,也能够用 -XX:+UseParNewGC 来指定应用 ParNew 作为新生代收集器。

3、Parallel Scavenge 收集器

Parallel Scavenge 也是一款用于新生代的多线程收集器,与 ParNew 的不同之处是,ParNew 的指标是尽可能缩短垃圾收集时用户线程的进展工夫,Parallel Scavenge 的指标是达到一个可管制的吞吐量。吞吐量就是 CPU 执行用户线程的的工夫与 CPU 执行总工夫的比值【吞吐量 = 运行用户代代码工夫 /(运行用户代码工夫 + 垃圾收集工夫)】,比方虚拟机一共运行了 100 分钟,其中垃圾收集破费了 1 分钟,那吞吐量就是 99%。比方上面两个场景,垃圾收集器每 100 秒收集一次,每次进展 10 秒,和垃圾收集器每 50 秒收集一次,每次进展工夫 7 秒,尽管后者每次进展工夫变短了,然而总体吞吐量变低了,CPU 总体利用率变低了。

收集频率

每次进展工夫

吞吐量

每 100 秒收集一次

10 秒

91%

每 50 秒收集一次

7 秒

88%

能够通过 -XX:MaxGCPauseMillis 来设置收集器尽可能在多长时间内实现内存回收,能够通过 -XX:GCTimeRatio 来准确管制吞吐量。

如下是 Parallel 收集器和 Parallel Old 收集器联合进行垃圾收集的示意图,在新生代,当用户线程都执行到平安点时,所有线程暂停执行,ParNew 收集器以多线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行;在老年代,当用户线程都执行到平安点时,所有线程暂停执行,Parallel Old 收集器以多线程,采纳标记整顿算法进行垃圾收集工作。

实用场景:重视吞吐量,高效利用 CPU,须要高效运算且不须要太多交互。能够应用 -XX:+UseParallelGC 来抉择 Parallel Scavenge 作为新生代收集器,jdk7、jdk8 默认应用 Parallel Scavenge 作为新生代收集器。

4、Serial Old 收集器

Serial Old 收集器是 Serial 的老年代版本,同样是一个单线程收集器,采纳标记 - 整顿算法。

如下图是 Serial 收集器和 Serial Old 收集器联合进行垃圾收集的示意图:

实用场景:Client 模式(桌面利用);单核服务器;与 Parallel Scavenge 收集器搭配;作为 CMS 收集器的后备预案。

5、CMS(Concurrent Mark Sweep) 收集器

CMS 收集器是一种以最短回收进展工夫为指标的收集器,以“最短用户线程进展工夫”著称。整个垃圾收集过程分为 4 个步骤:

  1. 初始标记:标记一下 GC Roots 能间接关联到的对象,速度较快
  2. 并发标记:进行 GC Roots Tracing,标记出全副的垃圾对象,耗时较长
  3. 从新标记:修改并发标记阶段引用户程序持续运行而导致变动的对象的标记记录,耗时较短
  4. 并发革除:用标记 - 革除算法革除垃圾对象,耗时较长

整个过程耗时最长的并发标记和并发革除都是和用户线程一起工作,所以从总体上来说,CMS 收集器垃圾收集能够看做是和用户线程并发执行的。

CMS 收集器也存在一些毛病:

  • 对 CPU 资源敏感:默认调配的垃圾收集线程数为(CPU 数 +3)/4,随着 CPU 数量降落,占用 CPU 资源越多,吞吐量越小
  • 无奈解决浮动垃圾:在并发清理阶段,因为用户线程还在运行,还会一直产生新的垃圾,CMS 收集器无奈在当次收集中革除这部分垃圾。同时因为在垃圾收集阶段用户线程也在并发执行,CMS 收集器不能像其余收集器那样等老年代被填满时再进行收集,须要预留一部分空间提供用户线程运行应用。当 CMS 运行时,预留的内存空间无奈满足用户线程的须要,就会呈现“Concurrent Mode Failure”的谬误,这时将会启动后备预案,长期用 Serial Old 来从新进行老年代的垃圾收集。
  • 因为 CMS 是基于标记 - 革除算法,所以垃圾回收后会产生空间碎片,能够通过 -XX:UserCMSCompactAtFullCollection 开启碎片整顿(默认开启),在 CMS 进行 Full GC 之前,会进行内存碎片的整顿。还能够用 -XX:CMSFullGCsBeforeCompaction 设置执行多少次不压缩(不进行碎片整顿)的 Full GC 之后,跟着来一次带压缩(碎片整顿)的 Full GC。

实用场景:器重服务器响应速度,要求零碎进展工夫最短。能够应用 -XX:+UserConMarkSweepGC 来抉择 CMS 作为老年代收集器。

6、Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采纳标记 - 整顿算法。能够与 Parallel Scavenge 收集器搭配,能够充分利用多核 CPU 的计算能力。如 Parallel Scavenge 中的两个垃圾收集器的搭配应用图。

实用场景:与 Parallel Scavenge 收集器搭配应用;重视吞吐量。jdk7、jdk8 默认应用该收集器作为老年代收集器,应用 -XX:+UseParallelOldGC 来指定应用 Paralle Old 收集器。

7、G1 收集器

上述的一些 GC 收集器通过并行与并发曾经极大的缩小了 STW 的工夫,然而 STW 的工夫还是会因为各种起因不可控,而 G1 提供的一个最大性能就是可控的 STW 工夫。

G1 通过把 Java 堆分成大小相等的多个独立区域,回收时计算出每个区域回收所取得的空间以及所需工夫的经验值,依据记录两个值来判断哪个区域最具备回收价值,所以叫 Garbage First(垃圾优先)。

这里有几个重要的概念:

  • Region(区域):G1 采纳了分区 (Region) 的思路,将整个堆空间分成若干个大小相等的内存区域,每次调配对象空间将逐段地应用内存。因而,在堆的应用上,G1 并不要求对象的存储肯定是物理上间断的,只有逻辑上间断即可;每个分区也不会确定地为某个代服务,能够按需在年老代和老年代之间切换。启动时能够通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是 2 的幂),默认将整堆划分为 2048 个分区。
  • Card(卡片):在每个分区外部又被分成了若干个大小为 512 Byte 卡片 (Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table) 中,调配的对象会占用物理上间断的若干个卡片,当查找对分区内对象的援用时便可通过记录卡片来查找该援用对象(见 RSet)。每次对内存的回收,都是对指定分区的卡片进行解决。
  • CSet(收集汇合):GC 过程记录的可被回收的 Region 的汇合。在 CSet 中存活的数据会在 GC 过程中被挪动到另一个可用分区,CSet 中的分区能够来自 eden 空间、survivor 空间、或者老年代。
  • RSet(Remembered Set 记忆汇合):记录了其余 Region 中的对象援用本 Region 中对象的关系,属于 points-into 构造 (谁援用了我的对象)。作用是不须要扫描整个堆找到谁援用了以后分区中的对象,只须要扫描 RSet 即可。
  • Humongous regions:用来寄存大于规范的 Region 内存 50% 的大对象区域,如果有些对象大于整个 Region 就会去找间断的 Region 保留,如果没有就会触发 GC。

G1 收集器与之前的收集器最大的不同就在于堆内存的划分,之前的收集器只辨别新生代与老年代,而 G1 收集器则是把堆内存划分成多个独立的 Region。

在上图中 G1 的 Java 堆中每个 Region 都有一个身份,每个 Region 有可能是 eden、survivor、old,然而他们的身份仅仅是逻辑上的,是能够变动的,G1 能够依据状况动静的调整各种 Region 的数量,通过管制回收的 Region 数量来管制 STW 的工夫,以达到 STW 工夫的可管制。

尽管 G1 收集器把 Java 堆化整为零成一个个 Region,然而也不会进行所有 Region 进行收集,G1 也分成了两种收集模式,两种模式如下:

Young GC: CSet 就是所有年老代外面的 Region;

Mixed GC: CSet 是所有年老代里的 Region 加上在全局并发标记阶段标记进去的收益高的老年代 Region;

Young GC 过程:

阶段 1:根扫描,动态和本地对象被扫描;

阶段 2:更新 RS,解决 dirty card 队列更新 RS;

阶段 3:解决 RS,检测从年老代指向老年代的对象;

阶段 4:对象拷贝,拷贝存活的对象到 survivorl/old 区域;

阶段 5:解决援用队列,软援用,弱援用,虚援用解决;

Mixed GC 过程:

1、全局并发标记(global concurrent marking)

2、拷贝存活对象(evacuation)

全局并发标记包含 5 个步骤:

1、初始标记(initial mark,STW):标记了从 GCRoot 开始间接可达的对象。

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

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

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

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

实用场景:要求尽可能可控 GC 进展工夫;内存占用较大的利用。能够用 -XX:+UseG1GC 应用 G1 收集器,jdk9 默认应用 G1 收集器。

GC 日志

每一种回收器的日志格局都是由其本身的实现决定的,换而言之,每种回收器的日志格局都能够不一样。但虚拟机设计者为了不便用户浏览,将各个回收器的日志都维持肯定的共性。

GC 日志是学 GC 调优之前的必备前置条件,所以咱们必须学会。上面放两张网图,大家能够从中看到日志的每个节点:

young gc 日志:

Full GC 日志:

文章首发于:
九神带你入门 JVM(下)

正文完
 0