JVM(三)——垃圾收集器简介

前言

上一篇博客的最后部分总结了垃圾收集的几种策略,其实我们有必要回头思考几个问题,为什么需要垃圾收集器,垃圾收集器如何收集“垃圾”对象?针对第一个问题其实比较好回答,因为Java本身不需要程序员显示的回收没有使用的对象,这也是Java风靡全世界的原因。针对第二个问题,我们可以从HotSpot实现垃圾回收算法的机制中得到答案。最后我们会简单总结一下目前JVM中常见的几种垃圾收集器。

HotSpot对垃圾收集器的一些思考

本节内容其实是《深入理解Java虚拟机》一书中的第三章第四节内容的一个自我总结。

如何快速标记

上一篇博客中介绍到,现在针对如何标记垃圾的操作,已经很少用所谓的引用计数法,而是采用的GC Root标记法,一些全局性的引用(比如常量和类静态属性)和执行上下文(操作数栈中的本地变量表中的变量)都可以作为GC root对象,但是我们需要面对的一个问题是:现在的应用超过几百兆是很正常的,对应的GC Root对象会很多,如果要标记所谓的“垃圾对象”难道要遍历程序中的每一个GC Root对象么?那样应用还跑个球,光是标记这些“垃圾”对象都耗时很长了。所以如何快速标记“垃圾对象”这个就是面对的第一个问题。

HotSpot虚拟机实现中其实是维护了一组称为OopMap的数据结构(具体长啥样我也不知道)。在类加载到JVM完成之后,HotSpot就把对象中的变量在内存中什么偏移量上是什么类型的数据计算出来,在编译过程中也会在特定的位置记录下栈和寄存器中那些位置是引用(OopMap中会专门记录那些地址中存放的是引用类型),这样就只需要通过OopMap能快速的标记堆中的对象。

什么是Stop The World

有了OopMap,HotSpot可以快速且准确的找到GC Root对象,但是我们还面对一个问题,程序在正常运行的时候,对象之间的引用关系一直在发生变化,但是我们在进行“垃圾对象”标记的时候,我们需要程序中对象之间的引用关系不发生变化,因此我们在进行垃圾对象标记的时候,需要程序停止一段时间,Sun将这件事情称为Stop the World。这就是STW的由来。即使在目前比较牛逼的CMS和G1垃圾收集器中,依旧会有STW的时间。不同的垃圾收集器STW的时间点和时间长度不同,常说的JVM调优,有一部分也是优化根据程序运行状态调整和优化STW的时间

何时建立OopMap

前面提到过通过OopMap能快速找到所谓的GC Root对象,但是另一个问题来了,何时建立这个数据结构呢?如果每一条指令我们都去创建这个OopMap,仔细思考一下会发现,其实建立OopMap的同时需要保证程序中的引用关系不变。那么这就回到了上面的问题(何时STW),这里就有了安全点这个概念(Safe Point)应用程序在安全点STW,然后开始记录OopMap,建立OopMap的过程在STW之前完成就可以了。回到上面一条,常说的JVM调优,有一部分也是优化根据程序运行状态调整和优化STW的时间,其实换一个角度来看其实就是对Safe Point个数的优化,Safe Point的选定即不能太少,也不能太多。

以上总结参考了博客:HotSpot中的OopMap、Safe Point和Safe Region

常见的垃圾收集器

下面可以开始总结一些常见的垃圾收集器了。提到常见的垃圾收集器,不得不提到《深入理解Java虚拟机》一书中的经典图例:

如果两个垃圾收集器之间存在连线,表示两个垃圾收集器可以进行搭配。下面根据垃圾收集器的类型进行一个简单的总结

串行回收器

所谓串行回收器,其实指的是每次回收的时候进行GC操作的只有一个线程,且GC操作的时候,会Stop the World。下图表示串行回收器的工作模式。

新生代串行回收器

上图中的Serial就是代表新生代的串行垃圾回收器,由于串行垃圾回收器在垃圾收集的时候会暂停用户线程,且进行GC操作的过程只有一个线程,在实时性要求较高的场景中并不适用,凡事都有两面性,串行化GC在运行成本较低的场景却少了线程切换的开销,却又有不俗的表现而且实现还特别简单,因此虽然一定程度上是一个古董,但是还有其存在的价值。在JVM的client模式下这个又是默认的垃圾收集器。

-XX:+UseSerialGC参数,指定使用新生代串行垃圾收集器和老年代串行垃圾收集器

老年代串行回收器

 上图中的Serial Old就是老年代的串行垃圾收集器。这里不再赘述串行回收器了,直接介绍那些参数会让老年代启用串行回收器

-XX:+UseSerialGC——新生代老年代都会用串行回收器

-XX:+UseParNewGC——新生代使用ParNew回收器,老年代使用串行回收器

-XX:+UseParallelGC——新生代使用ParallelGC回收器,老年代使用串行回收器

并行回收器

并行回收器不同于串行回收器,他说多个线程并发完成GC操作,有效的缩短了GC的耗时,但是线程切换也会带来一些维护上的成本。下面意图,简单示意了并行回收器的机制和模式。

新生代并行回收器

ParNew回收器

ParNew收集器其实就是Serial的多线程版本,Server模式下JVM的首选模式(1.7以前的版本是如此,也只有ParNew与CMS能配合工作,CMS是一款老年代的收集器,后续会介绍),ParNew由于需要维护多线程,其实也需要损耗一些维护成本,因此在单CPU环境下其实表现并不优秀,但是可以通过一些参数来控制其进行GC操作的线程,-XX:ParallelGCThreads参数即可指定进行进行GC操作的线程个数。不过一般最好指定的与CPU数量相当,不然CPU维护线程切换开销会耗费成本,影响GC性能。开启ParNew垃圾收集器的参数如下:

-XX:+UseParNewGC——新生代使用ParNew回收器,老年代使用串行回收器。
-XX:+UseConcMarkSweepGC——新生代使用ParNew回收器,老年代使用CMS收集器

ParallelGC回收器

ParallelGC是一个工作在新生代的并行垃圾收集器,这一点似乎与ParNew相同,回收操作采用复制算法。但是这个垃圾收集器最大的特点就是在于,其关注的吞吐量。所谓吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行应用程序代码时间 /(运行应用程序时间+垃圾收集时间)。例如运行应用程序耗时99ms,垃圾收集器花费1ms,则吞吐量为:99/(99+1)=99%。

高的吞吐量则可以高效率的利用CPU时间,尽快的完成程序的运算任务。因此ParallelGC较适合用于与单纯后台运算的程序。

新生代ParallelGC回收器有如下相关参数

-XX:+UseParallelGC——新生代使用ParallelGC回收器,老年代使用串行回收器
-XX:+UseParallelOldGC——新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器

同时还有两个参数用于控制系统的吞吐量

-XX:MaxGCPauseMills——设置最大垃圾收集停顿时间。指定一个毫秒数,JVM会在指定时间内完成垃圾回收,但是这并不意味着性能能得到提升,如果这个数值设置的过短,则GC的堆内存范围会变小。这也导致了会频繁GC会降低吞吐量。
-XX:GCTimeRatio——设置吞吐量大小。这个是一个整数,如果这个值设置为n,则系统将花费不超过1/(1+n)的时间用于垃圾收集器,如果GCTimeRatio为99,则1/(1+99)=1%的时间会用于垃圾收集器。

-XX:+UseAdaptiveSizePolicy——可以打开自适应的GC策略。这种模式下新生代的大小,各个分区的比例,晋升老年代的对象年龄等参数都会被自动调整,如果我们手动操作调整参数比较繁琐,这个参数倒是一个不错的选择

老年代并行回收器

ParallelOldGC回收器

这个是其实可以理解为ParallelGC回收器的老年代版本,相关参数不再赘述。

-XX:+UseParallelOldGC——新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器

CMS垃圾回收器

不管是上面介绍的串行垃圾收集器还是并行垃圾收集器,上面介绍的几种垃圾回收器依旧是串行标记的操作,并没有做到并行标记,真正意义上的并行标记垃圾收集器其实就是CMS垃圾收集器(CMS——Concurrent Mark Sweep)。因此从某种意义上来说,CMS垃圾回收器实现了扔垃圾的时候也能清理垃圾。这个垃圾收集器的工作过程与其他垃圾收集器相比,工作过程比较复杂。CMS工作时主要的步骤是:初始标记->并发标记->重新标记->并发清理->并发重置。

如果要理解CMS需要从每个阶段进行详细解读

初始标记阶段

初始标记阶段顾名思义,从名字来看,就是简单的进行一个标记,这是因为该阶段需要STW,因此考虑到GC停顿的耗时因此这个阶段只是简单标记直接与root相连的对象。如上图所示。

并发标记阶段

 这个时候用户线程依旧运行,同时CMS垃圾收集器同时进行垃圾对象的标记

由于用户线程处于运行,引用是有变化的。相对于上图a对象的引用就发生了变化。同时这个标记过程还会深入到下一步,标记到GC Root树结构的叶子节点。这对引用变化对象所在的区域,会别标记为dirty区域

重新标记

这个阶段需要STW。针对上一阶段出现的dirty区域,重新做一个扫描,如下图所示:针对上图中的dirty区域,C对象被标记为垃圾对象,g对象被重新引用标记。

 并发清理与重置阶段

这两个阶段依旧与用户线程并发,这个时候对象h会被回收,c对象会在重置阶段被回收

 相关参数

-XX:+UseConcMarkSweepGC——启用CMS垃圾回收器。CMS可以与ParNew垃圾收集器配合使用。

CMS是多线程回收器,设置回收的工作线程数也对系统性能有重要的影响。CMS的默认并发线程数是(ParallelGCThreads+3)/4。其中ParallelGCThreads新生代GC的线程数量,如果新生代使用的是ParNew垃圾回收器,则ParallelGCThreads则是新生代GC的线程数量,如果ParNew垃圾回收器的线程设置为3,则上述公式的结果为1。

-XX:ConcGCThreads或者-XX:ParallelCMSThreads两个参数可以手动设定CMS的线程数

以及回到CMS不是独占式回收器(整个GC过程都需要STW的回收器),在CMS的过程中应用程序依旧会不断的生产垃圾对象。这些垃圾对象是无法清理的(如上述示例中的c对象,并没有在清理过程被清理,而是在重置过程被清理)。因此在CMS回收过程中,还应该确保应用程序有足够的内存可用空间(换句话说,如果在CMS并发标记阶段内存突然垃圾爆满了,直接崩溃了,就尴尬了)。因此CMS并不会等到内存爆满的时候才回去清理垃圾,而是老年代内存使用达到一个阈值的时候就触发清理

-XX:CMSInitialitionOccupancyFraction——指定老年代触发回收的阈值默认值为68%,当老年代使用率达到68%的时候就会执行一次CMS。同时如果老年代内存使用实在是太快,CMS搞不定的情况下依旧会出现内存不足的情况下(可以理解为CMS失败),这个时候老年代就启用老年代串行化收集器进行垃圾回收(这也就是为什么在上述实例图中,老年代的垃圾回收器中CMS可以搭配Serial Old的原因)。

CMS依旧有他的毛病,毕竟是采用的标记清理的算法,因此会产生大量的内存碎片。产生了碎片,就不可避免的需要进行内存整理。

-XX:+UseCMSCompactAtFullCollection——可以使CMS在垃圾收集完成之后,进行一次内存碎片整理,这个不是并发进行的。
-XX:CMSFullGCsBeforeCompaction——可以设定进行多少次CMS回收之后,进行一次内存压缩整理。

G1垃圾收集器

1.7版本中正式使用的全新垃圾收集器,这个是为了取代CMS而存在的,其实在介绍CMS的时候,至少可以看出CMS有两个缺点:只能适用于老年代并且会产生内存碎片。G1通过采用全新的内存模型弥补了CMS的一些缺点。

G1的内存模型

G1采用了全新的内存模型,而不是我们之前理解的简单分区的结构了。

 从oracle中的参考文档中可以看到如下一张图

 堆被划分为一组大小相等的堆区域,每个区域都有一个连续的虚拟内存范围。 某些区域集被分配了与较旧的收集者相同的角色(eden,幸存者,旧角色),但是它们的大小没有固定。 这在内存使用方面提供了更大的灵活性。

G1的回收过程分为如下几个阶段:新生代GC->并发标记周期->混合收集->Full GC(可能)

新生代GC

G1收集器可以对新生代进行回收,既然是回收新生代,自然是回收eden和survivor区域,回收后所有的eden区域会被清空,老年代会增多。《实战Java虚拟机》一书中的这个图比较形象。

 G1并发标记周期

并发标记和CMS的似乎类似,这里实在不想在赘述,而且过程似乎比CMS要复杂的多。具体流程可用如下示意图来说明:

这里就单独说一下独占清理部分——这个阶段会引起STW,它将计算各个区域的存活对象和GC回收比例,并进行排序(G1——Garbage First),识别可供混合回收的区域。这个阶段,还会更新记忆集(Remember Set)。这个记忆集包含了下面混合回收所需要的信息

混合回收

这个阶段叫作混合回收,顾名思义,这个阶段即会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收——同时处理了新生代和老年代。但是针对这些区域的清理G1会优先回收垃圾比例较高的区域,因为回收这些区域效率也较高。

可能的Full GC

虽然很厉害,但是依旧需要老年代的Full GC(Serial GC old)进行兜底。

相关参数

-XX:+UseG1GC——启动G1收集器。

-XX:MaxGCPauseMillis——指定目标回收的最大停顿时间。如果任何一次停顿超过这个设置的时长,G1就会调整新生代和老年代的比例,堆大小,晋升策略等

-XX:ParallelGCThreads——用于设置并行回收时,GC的工作线程数量

-XX:InitiatingHeapOccupancyPecent——指定整个堆使用率达到多少的时候,触发并发标记周期的执行。默认为45%,堆使用超过45%,则执行并发标记周期。

ZGC

JDK 11中已经采用了令人惊叹的ZGC,平均一次GC操作不超过2ms,这确实是一个相当惊人的数据了。但是工作暂时使用不多,这里也不方便进行总结,毕竟实在是不太熟悉。

总结

参考主流的JVM书籍以及博客对JVM的各种收集器进行了简单总结,同时介绍了各个收集器中的相关参数,后续会进行对class文件进行一个总结

参考资料

关于G1其实有些许复杂,有些内容介绍的会比较详细

JVM性能调优实践——G1 垃圾收集器介绍

oracle G1

《实战JAVA虚拟机  JVM故障诊断与性能优化》

《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/103324479