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之类的。
希望看完文章以后,对你能有些帮助。
垃圾收集算法
介绍垃圾收集器之前,我们先了解一些垃圾收集器常用的算法和一些分代收集的理论。
-
分代收集理论
大部分收集器都遵循分代收集。JAVA堆划分出不通的区域后,垃圾收集器每次回收只针对某一区域或者某一部分区域(Minor GC,Major GC,Full GC)。也针对不通区域内对象的特征安排不通的垃圾回收算法。 -
标记-清除算法
算法分为'标记','清除'两个阶段:首先标记初所有需要回收的对象,在标记完成以后,统一回收所有被标记的对象,亦可以反过来。
缺点:
1.效率不稳定,执行效率会随着对象熟量的增长而降低
2.标记、清除后会产生大量不连续的内存碎片 -
标记-整理算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,衍生出的一种半区复制的垃圾收集算法。它将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当执行回收时,将存活的对象复制的另一块,然后一次性清理掉使用过的内存。这种算法如果碰到大量对象都是存活的时候,也会有大量的复制开销。
这种算法大多优先采用于回收'朝生夕灭'的新生代。 -
标记-复制算法
为避免大量存活对象时的复制开销,针对老年代提出了一种标记-整理的算法。这种算法和标记-清除类似,只不过标记-清除是不移动的。这种算法把存活的对象都向内存空间的一端移动,然后直接清理掉边界外的内存。
这样看来,和标记-清除对比,是否移动都是有利有弊的。移动时内存回收时会更复杂,不移动则内存分配更复杂(由于产生了大量的内存碎片);
有聪明的垃圾收集器会使用两种算法结合的方式去运行。平时使用标记-清除,当无法容忍内存碎片过多导致无法给大对象分配足够的内存空间时,采用标记-整理执行一次。
垃圾回收器的认识
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以上的了,所以这种组合弊端可以不用考虑。
- Parallel 这种"吞吐量优先"的收集器是不是理解也能用到对应的场景了~ 我们继续往下看
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都需要暂停用户线程。所以它并非追求低延迟,而是在延迟可控的情况下获得尽可能高的吞吐量。
- 这样看来是否所有的应用交互式网站都可以选择G1了?当然不是这样。可以这样想,G1的的功能强大的一部分原因是垃圾收集产生的内存占用和运行时的负载都比CMS高。所以小内存应用上,CMS的性能获取比G1的效果更优秀。
这个优劣的平衡点暂定在6G~8G之间。但是实际应用还得具体的应用场景。但是随着垃圾回收器的不断优化,相信会像G1去倾斜。
Shenandoah、ZGC、Epsilon收集器
这些收集器的设计更加的复杂,生产环境中都还没有使用过,目前没做深刻的理解。了解的也只是片面的,在这里就不做过多的说明了。后续学习到会补充上来。
选择适合的收集器
虚拟机为我们提供了多种垃圾收集器,我们选用的时候必须“因地制宜,按需选用”。文中穿插着介绍了各种收集器不通场景下的使用,但这也只是片面的,还是需要根据不同的场景去选择垃圾收集器以及配置响应的参数来优化我们的系统。
希望本文能对各位有所帮助。也欢迎各位指点给出意见。
本文参考《深入理解JAVA虚拟机》
网友评论