关于后端:JVM垃圾回收器及算法原理

34次阅读

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

有情怀,有干货,微信搜寻【三太子敖丙】关注这个不一样的程序员。

本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

前言

大家在面试的时候不同水平会被问到 JVM 的垃圾回收,看面试官程度,有些就背个书就行,比方 GC 的工作原理,有哪些 GC 算法和回收器,别离长处和毛病等等,有些面试官预计本人也就背书程度,都没个诘问;有些面试官就能诘问,一诘问就歇菜,比方低提早的垃圾回收器有哪些以及其原理,跨代援用及解决方案,三色标记及漏标问题解决,等等。

还是那句话,尽管都是些实践的问题,然而在理论开发过程中真的能遇到这些问题来解决理论问题,所以多多理解 JVM 的实现原理总没有错,既能抗极限面试,又能在适时的时候帮忙解决理论问题,失去领导和共事的赞叹,心甘情愿?

上面进入正题,先来个开胃菜,热热身。GC 的工作原理就不说了,要筹备面试的同学必须滚瓜烂熟,不然面试官要说出门右转了 …

垃圾回收算法

垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的形式都有不同,所以不须要去理解算法的具体实现。

复制算法

将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有按程序分配内存即可,实现简略,运行高效。

只是这种算法的代价是将内存放大为了原来的一半。然而要留神:内存挪动是必须实打实的挪动(复制),所以对应的援用(间接指针)须要调整。

复制回收算法适宜于新生代,因为大部分对象朝生夕死,那么复制过来的对象比拟少,效率天然就高,另外一半的一次性清理是很快的。

Appel 式回收

一种更加优化的复制回收分代策略:具体做法是调配一块较大的 Eden 区和两块较小的 Survivor 空间(个别称作做 From 区和 To 区,也能够叫做 S0 和 S1)

基于教训统计,新生代中的对象 98% 是“朝生夕死”的,所以并不需要依照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“节约”。当然,98% 的对象可回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保(Handle Promotion)

标记革除

算法分为“标记”和“革除”两个阶段:首先扫描所有对象标记出须要回收的对象,在标记实现后扫描回收所有被标记的对象,所以须要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率升高,因为须要大量标记对象和回收对象,比照复制回收效率要低。

它的次要问题,标记革除之后会产生 大量不间断的内存碎片 ,空间碎片太多可能会导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾回收动作。回收的时候如果须要回收的对象越多,须要做的标记和革除的工作越多,所以标记革除算法实用于 老年代

标记整顿

首先标记出所有须要回收的对象,在标记实现后,后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存。标记整顿算法尽管没有内存碎片,然而效率偏低。

咱们看到标记整顿与标记革除算法的区别次要在于对象的挪动。对象挪动不单单会减轻零碎累赘,同时须要全程暂停用户线程能力进行,同时所有援用对象的中央都须要更新(间接指针须要调整)。所以看到,老年代采纳的标记整顿算法与标记革除算法,各有长处,各有毛病。

垃圾回收器

回收器名称回收对象和算法回收器类型
Serial新生代,复制算法线程(串行)
Parallel Scavenge新生代,复制算法并行的多线程回收器
ParNew新生代,复制算法并行的多线程回收器
Serial Old老年代,标记整顿算法单线程(串行)
Parallel Old老年代,标记整顿算法并行的多线程回收器
CMS老年代,标记革除算法并发的多线程回收器
G1新生代,老年代;标记整顿 + 化整为零并发的多线程回收器

目前最罕用的两种垃圾回收器,也不必多说,必定是 CMS 和 G1,个别面试官会问下 CMS 和 G1 的区别以及各自的特点,不太会深刻问实现原理,毕竟 Java 面试可问的知识点切实太多了,都一个个深刻问 1 个小时的面试工夫基本不够。

串行的垃圾回收器就不说了,这里专门讲下并发的垃圾回收器

CMS(Concurrent Mark Sweep)回收器

顾名思义,这是并发的垃圾回收器,这种回收器是一种以获取最短的回收进展工夫为目标的垃圾收集器,目前很大一部分 Java 的互联网利用或者 B / S 零碎的服务器上,因为这类利用尤其在意相应速度,心愿零碎进展工夫越短越好,这样用户体验也会更好,CMS 就十分合乎这类利用的需要。

从名字就能够看出,这种回收器是基于标记革除的算法实现,它的运作过程绝对串行的垃圾回收器绝对简单点,分为以下 4 个步骤

初始标记:很短,仅仅只是标记下 GC Root 能间接关联的对象,速度极快。

并发标记:和用户利用同时进行,进行 GC Root 跟踪的过程,标记 GC Root 开始关联的所有对象,开始遍历整个可达剖析的门路对象,这个工夫比拟长,所以并发。

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

并发革除:因为整个过程中耗时最长的并发标记和并发革除过程收集器线程都能够与用户线程一起工作,所以,一般来说,CMS 的内存回收过程是与用户线程一起执行的。-XX:+UseConcMarkSweepGC,示意新生代应用 ParNew,老年代的用 CMS。

CPU 敏感:CMS 对处理器资源敏感,毕竟采纳了并发的收集、当解决外围数有余 4 个时,CMS 对用户的影响较大。

浮动垃圾:因为 CMS 并发清理阶段用户线程还在运行着,随同程序运行天然就还会有新的垃圾一直产生,这一部分垃圾呈现在标记过程之后,CMS 无奈在当次收集中解决掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。因为浮动垃圾的存在,因而须要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样期待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%)如果预留的内存不够寄存浮动垃圾,就会呈现 Concurrent Mode Failure,这时虚拟机将长期启用 Serial Old 来代替 CMS。

会产生空间碎片:标记 – 革除算法会导致产生不间断的空间碎片总体来说,CMS 是 JVM 推出了第一款并发垃圾收集器,所以还是十分有代表性。然而最大的问题是 CMS 采纳了标记革除算法,所以会有内存碎片,当碎片较多时,给大对象的调配带来很大的麻烦,为了解决这个问题,CMS 提供一个 参数:-XX:+UseCMSCompactAtFullCollection,个别是开启的,如果调配不了大对象,就进行内存碎片的整顿过程。这个中央个别会应用 Serial Old,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 产生这样状况会很卡。

总结:CMS 问题比拟多,所以 JDK 没有一个版本默认垃圾回收器是 CMS,只能手动指定。然而它毕竟是第一个并发垃圾回收器,对于理解并发垃圾回收具备肯定意义,所以咱们必须理解。为什么 CMS 采纳标记 - 革除,在实现并发的垃圾回收时,如果采纳标记整顿算法,那么还波及到对象的挪动(对象的挪动必然波及到援用的变动,这个须要暂停业务线程来解决栈信息,这样使得并发收集的暂停工夫更长),所以应用简略的标记 - 革除算法才能够升高 CMS 的 STW 的工夫。

该垃圾回收器适宜回收堆空间几个 G 至 20G。

G1(Garbage First)

随着 JVM 内存的增大,STW 的工夫成为 JVM 急切解决的问题,然而如果依照传统的分代模型,总跳不出 STW 工夫不可预测这点。

为了实现 STW 的工夫可预测,首先要有一个思维上的扭转。

G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都能够依据须要,表演新生代的 Eden 空间、Survivor 空间,或者老年代空间。

回收器可能对表演不同角色的 Region 采纳不同的策略去解决,这样无论是新创建的对象还是曾经存活了一段时间、熬过屡次收集的旧对象都能获取很好的收集成果。

Region:Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类非凡的 Humongous 区域,专门用来存储大对象。G1 认为只有大小超过了一个 Region 容量一半的对象即可断定为大对象。每个 Region 的大小能够通过参数 -XX:G1HeapRegionSize 设定,取值范畴为 1MB 至 32MB, 且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被寄存在 N 个间断的 Humongous Region 之中,G1 的进行回收大多数状况下都把 Humongous Region 作为老年代的一部分来进行对待。

开启参数 -XX:+UseG1GC
分区大小 -XX:+G1HeapRegionSize
个别倡议逐步增大该值,随着 size 减少,垃圾的存活工夫更长,GC 距离更长,但每次 GC 的工夫也会更长。

最大 GC 暂停工夫 -XX:MaxGCPauseMillis
设置最大 GC 暂停工夫的指标(单位毫秒),这是个软指标,JVM 会尽最大可能实现它。

运行过程如下:

初始标记: 仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中调配新对象。这个阶段须要进展线程,但耗时很短,而且是借用进行 Minor GC 的时候同步实现的,所以 G1 收集器在这个阶段理论并没有额定的进展。要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的调配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针,从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范畴。

并发标记 :从 GC Root 开始对堆中对象进行可达性剖析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描实现当前,并发时有援用变动的对象,这些对象会漏标,漏标的对象会被一个叫做 SATB(snapshot at the beginning) 算法来解决。

最终标记:对用户线程做另一个短暂的暂停,用于解决并发阶段结后仍遗留下来的最初那大量的 SATB 记录(漏标对象)。

筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间。这里的操作波及存活对象的挪动,是必须暂停用户线程,由多条收集器线程并行实现的。

总结:并行与并发:G1 能充分利用多 CPU、多核环境下的硬件劣势,应用多个 CPU(CPU 或者 CPU 外围)来缩短 Stop-The-World 进展的工夫,局部其余收集器 本来须要进展 Java 线程执行的 GC 动作,G1 收集器依然能够通过并发的形式让 Java 程序继续执行。

分代收集:与其余收集器一样,分代概念在 G1 中仍然得以保留。尽管 G1 能够不须要其余收集器配合就能独立治理整个 GC 堆,但它可能采纳不同的形式 去解决新创建的对象和曾经存活了一段时间、熬过屡次 GC 的旧对象以获取更好的收集成果。

空间整 合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整顿”算法实现的收集器,从部分(两个 Region 之间)上来看是基于“复 制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种个性有利于程序长时间运 行,调配大对象时不会因为无奈找到间断内存空间而提前触发下一次 GC。

谋求进展工夫:-XX:MaxGCPauseMillis 指定指标的最大进展工夫,G1 尝试调整新生代和老年代的比例,堆大小,降职年龄来达到这个指标工夫。

并发标记

三色标记算法

说到并发标记,就不能不提下并发标记中的三色标记算法,它是一种形容追踪式回收器的无效的方法,利用它能够推演回收器的正确性。

在三色标记法之前有一个算法叫 Mark-And-Sweep(标记革除)。这个算法会设置一个标记位来记录对象是否被应用。最开始所有的标记位都是 0,如果发现对象是可达的就会置为 1,一步步上来就会出现一个相似树状的后果。等标记的步骤实现后,会将未被标记的对象对立清理,再次把所有的标记位 设置成 0 不便下次清理。

这个算法最大的问题是 GC 执行期间须要把整个程序齐全暂停,不能异步进行 GC 操作。因为在不同阶段标记打扫法的标记位 0 和 1 有不同的含意,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的零碎来说,这种须要长时间挂起的标记打扫法是不可承受的。所以就须要一个算法来解决 GC 运行时程序长时间挂起的问题,那就三色标记法。三色标记最大的益处是能够异步执行,从而能够以中断工夫极少的代价或者齐全没有中断来进行整个 GC。

咱们将对象分为三种类型:

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

灰色:对自身被扫描,然而还没扫描完该对象的子对象。

红色:未被扫描对象,如果扫描完所有对象之后,最终为红色的为不可达对象,既垃圾对象。

以上图为例,简略说下三色标记的实现原理,首先如上图所示,线程 1 已实现所有标记,所有对象都被标记成彩色;线程 2 还处于半实现状态,其中对象 B 自身已被扫描,然而还没有扫描该对象的子对象。

因为垃圾回收的线程和失常业务线程都在执行中,并没有中断,如果此时业务代码如下:
A.c = C
B.c = null
则如下图

此时线程 1 因为曾经实现所有扫描,则对象 C 被脱漏标记为彩色;而线程 2 实现扫描,将对象 B 标记为彩色,线程 2 此时也实现所有扫描,问题就来了,对象 C 被漏标了。

对象 C 被漏标的间接结果就是被回收。然而它确实还须要被对象 A 援用,这就是三色标记中的漏标问题。

如何解决并发标记中的漏标问题?

CMS 的解决方案

Incremental Update 增量更新算法:

当一个红色对象被一个彩色对象援用,将彩色对象从新标记为灰色,让垃圾回收器从新扫描。

G1 的解决方案

STAB(snapshot at the beginning)算法:

开始做一个快照,当 B 援用 C 的关系隐没的时候要把这个援用推到 GC 的堆栈中,保障 C 还能被 GC 扫描到,最重要的是要把这个援用推到 GC 的堆栈,是灰色对象指向红色的援用,如果一旦某一个援用消失掉了,就会把它放到栈(GC 办法运行时数据也是来自栈中),JVM 其实还是能找到它的,下回间接扫描它就行了,那样红色就不会漏标。

对应 G1 的垃圾回收过程中的:最终标记 (Final Marking) 对用户线程做另一个短暂的暂停,用于解决并发阶段结后仍遗留下来的最初那大量的 SATB 记录(漏标对象)。

比照两种计划

SATB 算法是关注援用的删除。(B 对 C 的援用)

Incremental Update 算法关注援用的减少。(A->C 的援用)

G1 如果应用 Incremental Update 算法,因为变成灰色的成员还要从新扫,从新再来一遍,效率太低了。所以 G1 在解决并发标记的过程比 CMS 效率要高,这个次要是解决漏标的算法决定的。

目前各大公司根本都应用 CMS 或者 G1 作为服务的垃圾回收器,然而在应用过程中如果呈现一些奇怪的问题,有时候重现还特地难,思考下这些垃圾回收器的底层实现原理,没有百分之一百的完满计划,只有最适宜本人业务场景的计划。心愿大家在日常工作中多多思考,遇到问题能力迎刃而解。

我是敖丙,你晓得的越多,你不晓得的越多 ,感激各位人才的: 点赞 珍藏 评论,咱们下期见!


文章继续更新,能够微信搜一搜「三太子敖丙 」第一工夫浏览,回复【 材料】有我筹备的一线大厂面试材料和简历模板,本文 GitHub https://github.com/JavaFamily 曾经收录,有大厂面试残缺考点,欢送 Star。

正文完
 0