学会选择JVM垃圾收集器

2020-05-08  本文已影响0人  做量化的程序员

java面试经常会被问到JVM相关的一些问题。

  • Q:介绍几种JVM的垃圾收集器?
  • A:
    新生代:Serial 、ParNew 、Parallel Scavenge
    老年代:CMS 、Serial Old、Parallel Old
    还有全能型的G1(Garbage first)。

补充: 还有openJDK才有的Sheandoah收集器,Oracle正朔血统的ZGC,Epsilon。不过这些都是存在于高版本的JDK中(JDK11以上)
回答的不错,看来是了解过。

  • Q: 那你们是怎么选取的,或者针对不同的服务,应用场景是怎么选择的?
  • A: eeeeeee开始说不上来了,或者说一些统一配置G1之类的。

希望看完文章以后,对你能有些帮助。

垃圾收集算法

介绍垃圾收集器之前,我们先了解一些垃圾收集器常用的算法和一些分代收集的理论。

有聪明的垃圾收集器会使用两种算法结合的方式去运行。平时使用标记-清除,当无法容忍内存碎片过多导致无法给大对象分配足够的内存空间时,采用标记-整理执行一次。

垃圾回收器的认识

Serial收集器

这是最基础的垃圾收集器。在JDK1.3之前,年轻代只能用这种收集器。它工作模式为单线程,“单线程”不仅是它只会用一个处理器或者一条收集器去完成垃圾收集工作,而且还强调收集时必须暂停其他所有线程直到它执行完成。使用了标记-复制算法。

Serial Old收集器

Serial的老年代版本。单线程,使用标记整理算法。因为出发点和考虑方向和Serial类似,这里不做过多的阐述了。使用了标记-整理算法。

ParNew收集器

ParNew收集器除了支持多线程并行收集之外,其他与Serial 收集器相比并没有太多新之处。但是在JDK1.7之前,这个也是首选的新生代的收集器,可能是只有它能与CMS收集器配合使用的原因。ParNew收集器是激活CMS后(使用-Xx+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用XX:+/-UseParNewGC项来强制指定或者禁用它。使用了标记-复制算法。

Parallel Scavenge 收集器

Prallel Scavenge收集器也是一款新生代收集器 ,它同样是基于标记-复制算法实现的。同样也是并行处理的多线程收集器。
它的目标是达到一个可控的吞吐量,举个栗子,完成某个人物执行代码花了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99%。之前提到的以及CMS,G1等收集器都是为了尽可能减少垃圾回收而导致的用户线程暂停。
Parallel Scavenge 收集器提供了两个参数可以精确的控制吞吐量。最大停顿时间的-XX:MaxGCPauseMillis,或者-XX:GCTimeRatio直接控制吞吐量的比例。这里不做详细介绍了,可以自行查阅。

Parallel Old 收集器

Parallel Scavenge的老年代版本。支持多线程并发收集,基于标记-整理实现。注意这个收集器是JDK1.6才开始提供的。所以在这之前,Parallel Scavenge只能配合Serial Old使用。我想现在JDK应该都至少选用1.8以上的了,所以这种组合弊端可以不用考虑。

CMS(Concurrent Mark Sweep)

这是一个老年代收集器。它注重于尽可能减少暂停用户线程的时间。
它分为四个阶段:
1)初始化标记(仅仅只是标记一下GC Roots能直接关联到的对象,速度很快)
2)并发标记 (并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程)
3)重新标记 (为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象)
4)并发清除 (清理删除掉标记阶段判断的已经死亡的对象)
1,3这两步是需要暂停用户线程的,相对来说耗时较短。2,4两步是可以和用户线程并发执行的,我们来看一下这几步都做了些什么,为什么这么设计。

是不是感觉很香~

但没有完美的收集器,CMS也是有缺点的:
1)2,4阶段和用户线程并发执行,会影响一部分程序执行速度。
2)2,4阶段和用户线程并发执行过程中,产生的新垃圾需要等待下一次垃圾回收(浮动垃圾)。
3)前面提到过的CMS是采用标记-清理算法运行的,会产生大量的内存碎片。会在Full GC时进行标记-整理,这时候也是会暂停用户线程的。
CMS提供了很多参数来控制回收,比如这里不做阐述。主要你了解了回收机制,至于用什么参数就根据自己的情况配置了。

JDK9之后,慢慢的开始废弃CMS收集器。

G1收集器

被称为里程碑式的收集器。是一款“全功能”的垃圾收集器。
G1 收集器出现之前的所有其他收集器,垃圾收集的目标范围要么是整个新生代(MinorGC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(ollction Set, -般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一一个 Region都可以根据需要,扮演新生代的Eden空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、 熬过多次收集的旧对象都能获取很好的收集效果。Region中还有类特殊的 Humongous区域,专门用来存储大对象。
G1可以建立预测的停顿时间模型,根据每个Region里垃圾堆积的价值大小,根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis默认只200),优先处理回收价值最大的Region。
G1收集器可大致分为以下四步:
1)初试标记 (仅仅只是标记一下GC Roots能直接关联到的对象,速度很快)
2)并发标记 ( 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程)
3)最终标记 (用于处理并发结束后仍留下来的少量SATB记录)
4)筛选回收 (负责更新Region,回收垃圾。将需要清除Region中存活的对象移动到空的Region中,然后清理整个Region的空间)
其中1,3,4都需要暂停用户线程。所以它并非追求低延迟,而是在延迟可控的情况下获得尽可能高的吞吐量。

这个优劣的平衡点暂定在6G~8G之间。但是实际应用还得具体的应用场景。但是随着垃圾回收器的不断优化,相信会像G1去倾斜。

Shenandoah、ZGC、Epsilon收集器

这些收集器的设计更加的复杂,生产环境中都还没有使用过,目前没做深刻的理解。了解的也只是片面的,在这里就不做过多的说明了。后续学习到会补充上来。

选择适合的收集器

虚拟机为我们提供了多种垃圾收集器,我们选用的时候必须“因地制宜,按需选用”。文中穿插着介绍了各种收集器不通场景下的使用,但这也只是片面的,还是需要根据不同的场景去选择垃圾收集器以及配置响应的参数来优化我们的系统。

希望本文能对各位有所帮助。也欢迎各位指点给出意见。
本文参考《深入理解JAVA虚拟机》

上一篇 下一篇

猜你喜欢

热点阅读