各有所长的清洁工 —— Java虚拟机的垃圾收集器清单

原创 2017年08月27日 16:32:17

带着问题阅读

  • HotSpot都提供了哪些垃圾收集器?这些垃圾收集器各自都有什么特点?
  • 衡量一个垃圾收集器好坏的标准是什么?


导语

上一讲讲解了虚拟机进行垃圾收集的几种常用算法,这一讲,我们就要看看,这几种算法,或者说方法论,是如何在各种垃圾收集器中具体实现的,同时我们也将一起了解JVM中,到底有哪些垃圾收集器。

本文是Effective Java专栏Java虚拟机专题的第七讲,如果你觉得看完之后对你有所帮助,欢迎订阅本专栏,也欢迎您将本专栏分享给你身边的工程师同学。

在学习本节课程之前,建议您了解一下以下知识点:


垃圾收集器概览

本文讨论的垃圾收集器,是基于JDK 1.7 Update14之后的HotSpot虚拟机,这里面包含了大多数开发人员会遇到的垃圾收集器。这些收集器之间的关系如图所示:


这张Oracle上一篇博客的贴图,展示了7种作用于不同分代、各有各的特长的垃圾收集器。如果两个收集器之间有连线,则说明它们可以搭配使用。图中的问号,其实就是被HotSpot寄予厚望的G1收集器,下文会详细介绍。


关于垃圾收集器

在正式介绍各自收集器之前,有以下几点,是读者需要先了解一下的:


没有最好的垃圾收集器 只有最好的收集器组合

到目前为止,还没有什么最好的收集器,更加没有万能收集器。就像战场上没有一套战无不胜的战法一样,有的只是针对某一次战事的绝佳战法。因此,下文将会逐一介绍这些收集器的特性、基本原理和使用场景,教会读者,如何根据自己应用的特点和要求,去选择最佳的垃圾收集器搭配。文中提到的一些参数,读者了解一下即可,后面会有单独一节课程,来给大家演示如何使用这些参数。


评价垃圾收集器的指标

评价特定场合一下,一款收集器的好坏,主要有两个指标:停顿时间吞吐量

停顿时间是指进行垃圾收集时,用户线程的暂停时间,也就是之前课程所说的“Stop The World”,一般来说,用户交互较为频繁的B/S应用更为重视停顿时间的长短,停顿时间越短,用户等待时间就越少,体验就越佳。

吞吐量是指用于执行用户线程的时间占总应用时间的比率,对于无需和用户进行交互的纯后台应用来说,停顿时间没那么重要,更看重的是吞吐量的大小,吞吐量越大,说明执行用户线程的时间更长,处理速度就越高。


解释执行和即时编译器

JVM有两种方式去执行编译器编译出来的.class字节码文件——解释器(Interpreter)和即时编译器(Just In Time Complier)。

解释器就像一个老实本分的翻译家,逐字逐句的翻译,每遇到一个指令,就将它编译成本机的机器语言(Native Machine Code),然后执行,下次再遇到这一条指令,还会再编译一次;

而即时编译器,则像一个善于将外文翻译成地道的中文的翻译家,会对指令编译出来的机器语言进行执行效率的优化,并且把这个优化后的机器语言保存下来,下次遇到再这条的指令,就不需要编译,直接执行。

JVM运行时,默认是采用“混合模式”(Mixed Mode),也就是解释器和即使编译器搭配使用的方式。当需要程序迅速启动和快速执行时,解释器可以首先发挥作用,省去耗时的优化时间;而随着时间的推移,编译器会逐渐发挥作用,把越来越多的字节码编译并优化成本地代码,提高执行效率。


Client模式和Server模式

这是Java的两种运行模式,顾名思义,一个用在客户端,一个用在服务器端。

Client模式和Server模式不同点在于即时编译器的优化程度,当采用Client模式时,JVM只启用一个叫C1的即时编译器(Client Complier),这个即时编译器会对指令进行一些简单、可靠的优化;而当采用Server模式时,则会再启用一个叫C2的重量级即时编译器(Server Complier),会进行一些比较耗时的优化,甚至会做一些不可靠的激进优化

HotSpot会根据自身版本和宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或者“-server”来强制指定虚拟机运行在client模式或者server模式。

比如笔者在自己机器的命令行上直接敲入“java -version”, 可以看到是在serer模式下运行,并且采用mixed mode:


接着笔者敲入“java -client -version”,结果还是显示运行在Server模式:



根据Oracle上的资料,结论是:64位的操作系统上只能采用server模式。看来Oracle认为server模式的激进优化在64位的操作系统上是优于client模式的简单优化的。

网上还有资料说可以修改%JAVA_HOME%\jre\lib\amd64下的jvm.cfg文件,不过,在笔者64位的机器下,亲测无效:

jvm.cfg



执行“java -client -version”,报错:



ok,了解了以上概念之后,接下来让我们开始学习各种垃圾收集器。


Serial 收集器

Serial 收集器是最基本、历史最悠久的收集器。这是一个单线程的收集器,有以下两个特点:

  1. 只使用一个CPU或一条收集线程去完成垃圾收集;
  2. 进行垃圾收集时,必须暂停其他所有工作线程,也就是前面课程提到过的“Stop The World”;

这个单线程的收集器,是HotSpot运行在Client模式下的默认新生代收集器,主要是由于它有以下的优势:

  1. 对于单个CPU的环境来说,单线程的Serial 收集器不需要进行线程切换,减少了切换时的时间开销,因此在单CPU的环境下可以获得最高的收集效率;
  2. 对于大多数客户端桌面应用来说,分配给虚拟机的内存一般不大,新生代的垃圾一般在几十兆到一两百兆之间,停顿时间完全可以控制在几十毫秒到一百多毫秒以内,这点是可以接受的。

因此,Serial 收集器对于运行在客户端、单CPU环境的虚拟机来说,是一个很好的选择。


ParNew 收集器

ParNew 收集器其实就是Serial 收集器多线程版本,除了使用多条线程进行垃圾收集之外,其他都和Serial 收集器一致。

ParNew 收集器是虚拟机运行在Server模式下的首选新生代收集器,除了因为它是多线程收集器之外,还因为它是除了Serial 收集器,唯一一个能和CMS收集器配合的收集器。

ParNew 收集器在单CPU的环境中绝对不会有比Serial 收集器更好的收集效率,甚至由于线程切换的开销,它在通过超线程技术实现的两个CPU的环境中都不能百分百保证可以超越Serial 收集器。

当然,随着CPU数量的增加,ParNew 收集器的多线程优势会越发明显,它默认开启的收集线程数和CPU的数量相同,可以使用-XX:ParalletGCThreads参数来限制垃圾收集的线程数。


Parallel Scavenge 收集器

Parallel Scavenge 收集器,和ParNew 收集器一样,是新生代收集器、同样采用复制算法、同样是多线程收集,那么,它有什么特别之处呢?

Parallel Scavenge 收集器最大的特点是它的关注点在于获得一个可以控制的吞吐量。它提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills和直接设置吞吐量大小的-XX:GCTimeRatio.

MaxGCPauseMills的值越小,系统就会将新生代的大小调的越小,以加快垃圾收集的速度,但是这样也会增加了垃圾收集的频率,自然吞吐量就下去了。

GCTimeRatio是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。可以精确地控制吞吐量。

实际使用时,经常会使用这两个参数中的一个,再配合另一个参数,-XX:UseAdaptiveSizePolicy. UseAdaptiveSizePolicy是一个开关参数,这个参数打开之后,就不需要手工去指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio),虚拟机会根据当前系统的运行情况,动态调整这些参数,以实现你所设置的最大垃圾收集时间或者最大吞吐量的目标。这种有点人工智能、又有点傻瓜式的调节方式,叫做GC的自适应调节策略(GC Ergonomics).


Serial Old 收集器

Serial Old 收集器Serial 收集器的老年代版本,同样是单线程收集器,同样是为了给Client模式的虚拟机使用,使用“标记-整理”算法。

当运行在Server模式下时,它主要有两大用途:

  1. 在JDK 1.5以及之前的版本中,和Parallel Scavenge 收集器配合使用;
  2. 作为CMS收集器的后备方案,在发生Concurrent Mode Failure时使用,后面会详细介绍;


Parallel Old 收集器

Parallel Old 收集器Parallel Scavenge 收集器的老年代版本,采用多线程收集和“标记-整理”算法。

Parallel Old出现之前,如果新生代选择了Parallel Scavenge,老年代除了Serial Old就没有别的选择,而由于受到单线程的Serial Old在服务器端表现的拖累,使用Parallel Scavenge也未必可以获得吞吐量最大化的效果。

Parallel Old 收集器的出现让“吞吐量优先”收集器终于有了合适的应用组合


CMS 收集器

CMS(Concurrent Mark Sweep)收集器的定位是获取最短的"Stop The World"的时间,也就是最短停顿时间,在具有大量用户交互使用的B/S应用上,停顿时间越短,就越能给用户带来好的体验。那么CMS是如何做到最短停顿的呢?

答案是并发。CMS实现了垃圾收集线程和用户线程的并发执行,从名字上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程有以下4个步骤:

  1. 初始标记(CMS initial mak)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中,初始标记和重新标记是仅有的两个需要“Stop The World”的阶段。其他两个阶段都不需要。

初始标记只是标记以下GC Roots能直接关联的对象,也就是下图中和GC Roots有直接连线的object1,因此速度很快;

并发标记就是进行GC Roots Tracing,对所有与GC Roots不可达的对象进行标记,下图中,object5和object6将会在这个阶段被标记;


重新标记则是为了修正并发标记期间,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的时间一般比初始标记的长,但远比并发标记短;

并发清除,这个阶段JVM将启动多条线程将所有标记为unreachable的对象清除掉。

整个过程,耗时最长的并发标记和并发清除都可以与用户线程同时工作,因此能够实现最短的停顿时间。

CMS绝对是一款十分优秀的收集器,并发收集、低停顿,但是CMS还远不到完美,它有以下3个明显缺陷:

1、CMS会抢占CPU资源。并发阶段虽然不会导致用户线程暂停,但却需要CPU分出精力去执行多条垃圾收集线程,从而使得用户线程的执行速度下降。

2、CMS无法处理浮动垃圾(Floating Garbage),可能会出现“Concurrent Mode Failure”而导致另一次Full GC。并发清理的过程中,由于用户线程还在执行,因此就会继续产生对象和垃圾,这些新的垃圾没有被标记,CMS只能在下一次收集中处理它们。这也导致了CMS不能在老年代几乎完全被填满了再去进行收集,必须预留一部分空间提供给并发收集时程序运作使用。在JDK1.5默认设置下,老年代使用了68%(JDK1.6是92%)的空间后CMS的垃圾收集就会被激活,其实这是一个比较保守的设置,只要应用中老年代增长不是很快,可以适当地调高参数-XX:CMSInitialingOccupancyFraction来提高触发百分比,降低回收的频率来获得更好的性能。如果CMS在收集期间,内存无法满足程序的需要,就会出现“Concurrent Mode Failure”,这时JVM将启动Plan B,也就是临时调用单线程的Serial Old收集器来重新进行老年代的垃圾收集,这样的话,CMS原本降低停顿时间的目的不仅没完成,和直接使用Serial Old收集器相比,还增加了前面几个阶段的停顿时间。

3、CMS的“标记-清除”算法,会导致大量空间碎片的产生为什么?)。碎片的积累会给分配大对象带来麻烦,往往会出现明明老年代还有很多空间剩余,但是却无法找到连续的空间分配对象的情况,这时候就不得不触发一次Full GC。为了解决这个问题。CMS提供了一个-XX:+UseCMSCompactAtFullCollection的开关参数(默认是开启的),用于在CMS收集器进行Full GC时对内存碎片进行合并整理,整理的过程是需要暂停用户线程的,这样碎片虽然没有了,但停顿时间又变长了。CMS的设计初衷可是降低停顿,于是又提供了一个参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩碎片的Full GC后,跟着来一次带压缩的Full GC(默认值为0,即每次都会)。

到这里终于把伟大的CMS介绍完了,接下来还有更厉害的G1收集器 :)


G1 收集器

G1(Garbage First)收集器是当代收集器技术发展的最前沿成果之一,是一款面向服务端的收集器,HotSpot团队甚至希望G1收集器在未来可以替换掉CMS收集器。

G1收集器具有以下特点:

并行和并发:这一点和CMS是类似的,可以充分利用CPU的资源,来缩短“Stop The World”的时间,提高收集效率。

不产生空间碎片:和CMS的“标记-清除”算法不同,G1从整体上看是采用“标记-整理”,从局部又像是“复制”,但无论如何,它都不会像CMS一样产生大量碎片而导致分配大对象失败的情形。

可预测的停顿:这又是G1相对于CMS的一大优势,CMS和G1都追求最低停顿时间,但是G1可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

G1收集器为什么可以做到可预测的停顿时间呢?

想一下,学渣在考试时怎样拿高分?肯定不是一道道题按顺序做下去,拿起卷子,先看一遍,哪些自己会的,先拿下再说。

G1收集器之所以可以做到可预测的停顿,当然就不是像其他收集器一样“死板”,一定要把所有垃圾全部清理了才罢休,G1收集器将内存分成了很多个Region,并对每个Region的垃圾收集价值进行跟踪记录(通过回收到的对象大小和时间的经验值进行计算),每次在允许的时间内,优先对回收价值大的Region进行回收,保证了在有限的时间内获得尽可能高的回收效率,这也是为什么它被叫做Garbage First的原因。


总结

这一讲,介绍了HotSpot虚拟机中的七种垃圾收集器,最后用一张图总结一下,方便大家复习:


这一讲篇幅较大,里面的一些知识点,譬如client模式和Server模式、解释器和即时编译器、各种垃圾收集器的参数等,没有展开讲解,后续课程会逐一做详细介绍。


课后思考题

我在学习CMS收集器时,当时就有一个疑问:为什么CMS的“Concurrent Sweep”阶段,不需要暂停用户线程?假如remark阶段被标记为unrechable的对象,在“Concurrent Sweep”时,被用户线程重新指向一个对象,这时候“Concurrent Sweep”如果将它清除,不就会导致空指针了?欢迎在评论区写下您的答案,O(∩_∩)O谢谢。


参考文献



 


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Java内存区域剖析 —— 定位OutOfMemory异常之前的必修课

在学习如何定位这些异常发生的原因并提出解决方案之前,我们必须了解一下,Java虚拟机是如何划分自己的内存区域的。

JAVA 模拟瞬间高并发

前些日子接到了一个面试电话,面试内容我印象很深,如何模拟一个并发?当时我的回答虽然也可以算是正确的,但自己感觉缺乏实际可以操作的细节,只有一个大概的描述。       当时我的回答是:“线程全部在同一...

java模拟并发请求测试方法是否线程安全

java模拟并发请求测试方法是否线程安全

Java模拟并发操作进行压力测试代码

import java.io.BufferedReader;   import java.io.File;   import java.io.FileInputStream;   import ...

用小说的形式讲解Spring(2) —— 注入方式哪家强

构造器注入和set注入,到底选哪个好呢

Java OutOfMemory异常清单 —— 在自己的机器上制造内存溢出

既然我们知道各个内存区域存储的内容,那么只要在代码上做一些手脚,就可以制造出OutOfMemory异常,这就是我们这一讲要做的事。

一起走进Java虚拟机的世界 —— 为什么要弄懂虚拟机

从本周开始,专栏Effective Java将开启一个全新的专题——Java虚拟机,在这个专题的课程里,您将学到如何定位OutOfMemory异常、如何进行JVM调优之类的知识,本文作为专题的开篇,带...

java CountDownLatch 模拟多并发线程简单例子

CountDownLatch作用  CountDownLatch,是一个倒计数的锁存器,当计数减至0时触发特定的事件。 构造方法参数指定了计数的次数 countDown方法,当前线程调用此方...

java CountDownLatch 模拟用户并发请求

java.util.concurrent.CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 主要方法  public Count...

用画小狗的方法来解释Java中的值传递

用生动有趣而又具有深度的方式讲解Java中的值传递。
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)