看完这篇,我不再怕面试官问垃圾收集了
「说在前面」:本文的篇幅较长,看本文的时候最好先去上个厕所,先准备好一杯枸杞茶,慢慢品,本文将会讲解三种垃圾收集算法:「标记-清除、复制、标记-整理算法」,以及各类成熟度较高的垃圾收集器:「Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS以及G1(Garbage First)」,若是有任何错误的地方,欢迎指出!html
❝在讨论垃圾收集算法以前,须要先了解针对不一样区域进行收集的名词:
❞Minor GC
(新生代收集)、Major GC
(老年代收集)、Full GC
(整个Java堆和方法区的收集)、Mixed GC
(新生代收集和部分老年代的收集,目前只有G1收集器有这种行为)web
垃圾收集算法
前一篇文章介绍了可达性分析算法后,了解了虚拟机会利用可达性分析来识别哪些对象是垃圾,能够回收,那么是如何进行回收的呢?下面就会介绍这三种垃圾收集算法面试
标记-清除算法(Mark-Sweep)

能够看到,在对象能够被回收的区域上,JVM会直接把这些「垃圾对象」占用的内存直接清除掉。算法
这个算法的优势很明显:「简单」浏览器
这个算法也有许多缺点:安全
❝执行效率不稳定,若是Java堆中包含大量对象,某一次回收时无用的对象很是多,这时候会花费不少时间进行内存的清除。服务器
有可能形成内存空间碎片,上图只是一个理想的删除过程,正好没有内存碎片产生,而实际上在内存中待清除的内存有可能不是连续的,致使会产生许多内存碎片,若是某个大对象没法找到一块连续的内存进行存放时,会误觉得堆内存不足,提早触发
❞Full GC
微信

因此为了「解决内存碎片问题」,科学家们研制出了一种新的算法:标记-复制算法数据结构
标记-复制算法(Mark-Copying)

由上面的动图能够看出,标记-复制算法将本来的「堆内存划分了两个区域」,采用了“半区复制”算法,将一半的内存省出来,当发生垃圾收集行为时,将存活的对象复制到另一半保留区域中「连续存放」。多线程
标记-复制算法的优势是解决了「大对象」分配内存的内存碎片问题
,也解决了标记-清除算法中大量垃圾对象致使的清除效率问题
。
缺点也很是的明显,那就是「可分配的内存空间少了整整一半」,并且若是某次存活的对象较多,甚至「所有存活」,那么复制的效率将会很是低。
标记-整理算法(Mark-Compact)

为了提高内存的利用率,科学家提出了标记-整理算法,该算法的起始过程和标记-清除
算法相同,先标记处待回收对象的内存区域,可是在清除时不是对全部可回收对象清除,而是「让全部存活对象往内存空间的一边移动」,把存活对象边界外的内存直接清空掉。
标记-整理算法「提升了内存的利用率」、解决了「大对象分配时的内存碎片问题」,看似完美的垃圾收集算法,也有它的弊端
❝在移动存活对象的过程当中,须要全程暂停用户程序的执行,被设计者称为“「Stop The World」”。
❞
分代收集
新生代垃圾收集及内存分配

分代收集算法本质上「标记-复制算法」,它把堆内存中较大的一块区域做为「新生代区域」,新生代区域中分为一个Eden区域和两个Survivor区域,Eden和Survivor的比例默认是「8:1」,由于在Eden区域,绝大数对象都熬不过第一轮GC(98%),因此每一个Survivor区域只须要10%的空间就足矣了,每一次触发Minor GC
时,就会将Eden区和Survivor区存活的对象复制到另一个Survivor区域中,而后清除掉被回收的对象,每次都依据这样的步骤进行垃圾收集。
不知道你有没有注意到每一个对象有一个数字的标记,这个标记是「对象的年龄」,当对象到了「15岁之后」(默认状况)就会被晋升为「老年代」
晋升老年代

如图所示,当对象在Survivor区存活了15次之后,就会晋升为老年代对象。
还有如下状况会晋升为老年代对象:
❝「大对象」。当对象所占连续内存很是大时,不会分配在Eden区,若是分配在Eden区,那么对象存活时产生的复制操做将致使效率大大下降。
若是在Survivor区,相同年龄的「对象总大小」大于「Survivor区空间的一半」时,也会将这些年龄相同的对象直接晋升到老年代,缘由也是防止对象的复制操做致使的效率问题。
❞
空间分配担保
在对象没法分配到Eden区时,会触发一次Minor GC
,JVM会首先检查「老年代最大的可用连续空间」是否大于「新生代全部对象的总和」,若是大于,那么此次Minor GC
是安全的,若是「不大于」的话,JVM就须要判断HandlePromotionFailure
是否容许空间分配担保。
若是容许担保,则证实老年代的连续可用内存空间大于历次晋升到老年代对象的平均大小,此时触发一次Minor GC
,若是小于,那么证实老年代并无把握放得下Survivor区有可能晋升的对象,此时发生一次Full GC
。
Stop The World
发生GC
(MinorGC或者FullGC)时,都会将用户线程停顿并进行垃圾收集,在Minor GC
中,STW
的时间较短,只涉及Eden
和survivor
区域的对象清除和复制操做,而Full GC
则是对整个堆内存进行垃圾收集,对象的扫描、标记和清除操做工做量大大提升,因此Full GC
会致使用户线程停顿较长时间,若是频繁地发生Full GC
,那么用户线程将没法正常执行。
或者通俗的理解:
❝你给你妈妈打扫房间时,你是但愿她坐在一旁静静等你扫完地再继续活动,仍是想你一边扫地,她一边丢垃圾呢?
❞
Safe Points
既然要「用户线程停顿下来」,那么要在什么地方停顿呢?JVM采用「主动式中断方式」告诉Java线程须要停顿了,JVM在特定的位置设置了这些安全点(Safe point),让线程能够在这些安全点主动挂起。
❝方法调用、循环跳转、异常跳转
❞
这些安全点的特征是「令程序有可能进行某一段长时间执行的特征」。
在这些安全点上存有对象引用信息的OopMap
数据结构,这种数据结构你能够理解为HashMap
这种数据结构,它内部存储了什么位置上存储了对象引用信息,这些信息在类加载完成时就肯定下来了。因此JVM在垃圾收集时不须要从一个个方法的GC Roots
去扫描,从OopMap
中能够快速准确地定位到这些GC Roots
。
❝若是用户线程自己处于停顿状态,例如阻塞(Blocked)、睡觉(Sleep),那么此时触发GC时,用户线程没法响应JVM的中断(我听不见你喊我,我睡着了~),用户线程没法主动地跑去安全点中断挂起,此时该怎么办呢?
❞
对于这种状况,必须引入「Safe Region」来解决。
Safe Region
安全区域是指,用户线程进入某一段代码区域中时,引用关系不会发生变化,那么在这片代码区域的任何地方开始GC都不会受到影响。实现的方式是,用户线程进入安全区域时「会标识本身已经进入安全区域」,在JVM发起GC时「没必要理会那些已经标识为进入安全区域的线程」,当用户线程「须要离开安全区域时」,会主动检查JVM是否已经完成了「须要停顿线程的工做」,若是已完成则能够离开,若是未完成则「必须一直等待」,直到JVM发送能够离开安全区域的信号为止。
垃圾收集器
垃圾收集器分为新生代收集器与老年代收集器,各类不一样的收集器之间若是符合标准则能够相互搭配使用
新生代收集器
Serial收集器

Serial收集器是一款单线程的垃圾收集器,“单线程”的「意义」不只仅是指它只能用一条线程或占用一个处理器去完成垃圾收集操做,更重要的是它进行垃圾收集时,**须要暂停其它全部线程,直到垃圾收集结束。**它身为最古老的一款垃圾收集器,在当今依旧普遍受用,它有如下优势:
❝对于内存受限的环境,它是全部收集器里额外内存消耗最小的
没有线程交互的开销,Serial收集器能够很好地专一于收集垃圾,把用户线程都停掉
❞
在用户桌面的应用场景和近年来流行的部分微服务应用中,分配给虚拟机管理的内存通常不会特别大,收集几十兆、一两百兆的新生代(桌面应用的新生代甚至少于这个容量),垃圾收集彻底能够控制在十几、几十毫秒,最多一百毫秒,这点停顿时间对用户来讲是十分友好的。
ParNew收集器

ParNew是一款「并行新生代收集器」,parNew收集器除了支持多线程并行收集之外,「其他的行为与Serial收集器彻底一致」,包括收集算法、STW(Stop The World)、对象分配规则、回收策略等等。
parNew是很多运行在服务器端模式下的HotSpot虚拟机中首选的新生代收集器,其中一个与性能、功能无关但很重要的缘由是:「除了Serial收集器,只有ParNew可以与CMS收集器配合工做。」
CMS收集器与Parallel Scavenge收集器不能配合工做的一个缘由是:Parallel Scavenge收集器内部并「没有按照分代收集的框架进行设计垃圾回收」,在以后的「G1收集器」也一样没有按照分代回收的框架设计。
Parallel Scavenge收集器
Parallel Scavenge收集器一样是基于标记-复制算法实现的收集器,也是可以并行收集的一款新生代收集器,那它与ParNew收集器的「差异在哪里呢?」
Parallel Scavenge收集器的特别之处在于它与其它收集器的关注点不同,其它垃圾收集器关注如何「最大限度地减小STW的时间」,而Parrel Scavenge关注的是「如何达到一个可控制的吞吐量(Throughput)」,因为与吞吐量关系密切,因此也被称做“吞吐量优先收集器”。
Parallel Scavenge收集器能够实现「自适应策略」,这是另一个与ParNew收集器的差异,能够经过指定-XX:UseAdaptiveSizePolicy
参数,虚拟机就会根据系统当前的运行状况收集监控信息,而且「自动调整系统的相关JVM参数以提供最高的吞吐量和最合适的停顿时间」。
老年代收集器
Serial Old收集器

使用标记-整理
算法,是一个单线程收集器,它有另外两个用途:
❝它做为CMS收集器发生失败后的后备预案,在CMS收集器并发收集发生Concurrent Mode Failure使用
做为Parallel Scavenge的老年代收集器
❞
这个时候就有疑惑了,Parallel Scavenge
收集器不是没有按分代收集框架实现吗,为何可以搭配Serial Old
收集器使用
《深刻理解Java虚拟机》:Parallel Scavenge
收集器架构中含有PS MarkSweep
收集器进行老年代收集,并不是直接调用Serial Old
收集器,可是PS MarkSweep
与Serial Old
的实现几乎是同样的,因此官方不少地方用Serial Old
代替它进行讲解。
Parallel Old收集器

Parallel Old
是Parallel Scavenge
的老年代版本,支持多线程并发收集,基于标记-整理
算法设计,自从JDK6之后,Parallel Old
和Parallel Scavenge
成为了最好的搭档,在「注重吞吐量或者处理器资源比较紧缺」的状况下,均可以采用这个组合。
CMS收集器
CMS收集器是基于获取「最短回收停顿时间」为目标的收集器,CMS收集器适合追求服务的响应速度的应用,例如基于浏览器的B/S系统的服务端上。
CMS是基于标记-清除
算法设计的,它支持用户线程与GC线程并发执行,以下图所示

运做过程分为4个阶段:
❝初始标记、并发标记、从新标记、并发清除
❞
初始标记的过程就是扫描GC Roots;
并发标记是扫描GC Roots链上全部的对象,此时会出现一些对象标记的变更,由于用户线程仍然在执行;
从新标记的过程是修正并发标记期间产生引用变更的那一部分对象的标记记录
并发清除是删除掉标记阶段判断已经死亡的对象,因为不用移动存活对象,此时也是能够并发执行的。
CMS收集器有三个缺点:
-
对处理器资源特别敏感,因为是并发执行,因此CMS收集器工做时会占用一部分CPU资源而致使用户程序变慢,下降总吞吐量,建议具备四核处理器以上的服务器使用CMS收集器
-
CMS没法清除浮动垃圾,有可能出现
Concurrent Mode Failure
失败而致使另外一次STW
的Full GC
产生。因为并发清理过程当中用户线程与GC线程并发执行,就必定会产生新的垃圾对象,可是没法在本次GC中处理这些垃圾对象,不得不推迟到下一次GC中处理,这些垃圾对象就称为“浮动垃圾”,到JDK6的时候,CMS收集器启动阈值达到92%
,也就是老年代占了92%
的空间后会触发GC,可是若是剩余的内存8%
不足以分配新对象时,就会发生“并发失败”,进而冻结用户线程,使用Serial Old
收集器进行一次Full GC
,因此触发CMS收集器的阈值仍是根据实际场景来设置,参数为-XX:CMSInitiatingOccu-pancyFraction
。 -
基于
标记-清除
算法会致使内存碎片不断增多,在分配大对象时有可能会提早触发一次Full GC
。因此CMS提供两个参数可供开发者指定在每次Full GC
时进行「碎片整理」,因为碎片整理须要移动对象,因此是没法并发收集的,-XX:+UseCMSCompactAtFullCollection
(JDK9开始废弃),-XX:CMSFullGCsBeforeCompaction
(JDK9开始废弃,默认值是0,每次Full GC都进行碎片整理)。
Garbage First收集器

这是一个在垃圾收集器技术发展历史上的里程碑式的成果,它取代了Parallel Scavenge + Parallel Old
的组合,并取代了CMS
,做为它们的继承者和替代者,G1到底有什么魔力呢?
❝G1是一种“「停顿时间模型」”收集器,也就是说能够指定在时间片断为
M
毫秒时,垃圾收集所占用的时间不会超过N
毫秒。G1颠覆了以前的全部垃圾收集器的垃圾收集行为:要么新生代收集(Minor GC)、要么老年代收集(Major GC)、要么整堆收集(Full GC),而G1能够面向「堆内存任何部分组成回收集」(Collection Set , CSet),衡量标准再也不是它属于哪一个分代,而是「哪块内存存放的垃圾数量较多」,这就是G1所特有的Mixed GC模式。
❞
能够看到上图中每个方块就是一个Region,每一个Region能够存放1~32MB大小的对象,使用参数-XX:G1HeapRegionSize
指定,Region中能够存放Eden
/Survivor
/Humongous
/Old
,G1中新生代和老年代并非连续存放的,而是一个动态的集合。
注意在G1中专门用Region
存放一个Humongous
大对象,当对象容量大于Region的一半时就认为它是大对象,按照“大对象优先在老年代中分配”,Humongous
也是老年代的一部分对象。
G1收集器将Region
单元看出是最小的内存回收单元,每次发生GC时,G1收集器都会评估各个Region
的「价值大小」,根据用户所指定的收集停顿时间来优先处理那些回收价值最大的Region
,这也是Garbage First
的由来。

G1收集器的运做过程能够分为4个步骤:
❝「初始标记」:仅记录GC Roots对象,须要停顿用户线程,但时间很短,借助
Minor GC
同步完成。「并发标记」:从GC Roots开始遍历扫描全部的对象进行可达性分析,找出要回收的对象,因为是并发标记,有可能在扫描过程当中出现引用变更。
「最终标记」:将并发标记过程当中出现变更的对象引用给纠正过来。
「筛选回收」:对各个Region的回收价值和成本进行排序,根据用户所但愿的停顿时间来制定回收计划,选取任意多个Region区域进行回收,把回收的Region区域中的存活对象复制到空的Region区域中,而后清空掉原来的Region区域,涉及对象的移动,因此须要暂停用户线程,由多条GC线程并行完成。
❞
如何设置G1的停顿时间?
G1的停顿时间不能太短,若是停顿时间太短,那么每次GC收集都只会回收占用Region内存区域很小的一部分,而随着内存不断分配,堆上的垃圾愈来愈多,GC的速度低于分配的速度,就会触发Full GC
,因此,只要咱们把停顿时间设置后的效果为「垃圾回收的速度与内存分配的速度大体相同」,那么在理论上来讲就永远不会发生Full GC
,「这也是G1被称为很牛逼的一个地方。」
G1和CMS的比较
❝G1从总体上看是“标记-整理”算法,从局部(两个Region之间)上看是“标记-复制”算法,不会产生内存碎片,而CMS基于“标记-清除”算法会产生内存碎片。
G1在垃圾收集时产生的内存占用和程勋运行时的额外负载都比CMS高
G1支持动态指定停顿时间,而CMS没法指定
二者都利用了并发标记这个技术
❞
总结
本文主要介绍了各类垃圾收集算法以及当前较为成熟的垃圾收集器,其中G1和CMS这两款垃圾收集器是最受关注的,解释了为何在垃圾收集时须要Stop The World
,本文篇幅较长,能读到这里是很是不容易的,以后也要多加复习!CMS和G1是很是火的两款收集器,可是碍于时间,本文尚未很详细地介绍,以后应该还会出一篇针对目前最流行的垃圾收集器的新特性,还会详细比较与CMS和G1收集器之间的异同!
参考资料:
《深刻理解Java虚拟机》
https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/
https://www.cnblogs.com/yangchunchun/p/7405502.html
https://blog.csdn.net/ladymorgana/article/details/82352100
https://mp.weixin.qq.com/s/_AKQs-xXDHlk84HbwKUzOw
结尾
感谢大家阅读我编写的推文!笔者的能力有限,太过于具体和细节的知识暂时还没法掌握,若是我有任何写得不正确和不许确的地方,欢迎你们向我提出来,咱们能够一块儿学习和交流!

本文分享自微信公众号 - 小菠萝的IT之旅(jie534838084)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。