之前我们简单提到了 JVM 中的垃圾收集算法,比较常见的包含有标记-清除算法,标记-复制算法,标记-整理算法,那么针对这些算法的各种具体实现也就有了 JVM 中使用的各种垃圾收集器,对于这些垃圾收集器,简单一点可以直接按照新生代和老年代来进行划分,针对新生代中使用的垃圾收集器有: Serial,Parallel New,Parallel Scavenge,针对老年代中使用的收集器有:Serial Old,Parallel Old,CMS(Concurrent Mark Sweep)。正是因为没有一款收集器能够适用于所有场景,所以才有了各种场景下的收集器出现。
目前随着 JVM 版本的不断迭代更新,同时也对垃圾收集器的不断优化,最新的已经出现了一些更为优秀的垃圾收集器,比如说 **G1(Garbage First)**,它是一个横跨新生代和老年代的的垃圾回收器;同时还有宣称能够将暂停时间控制在 10ms 以内的 **ZGC(Z Garbage Collector)**,这些都是随着我们内存越来越大,然后对于低延迟的不断追求而产生的一系列优秀的收集器。
新生代收集器
Serial 收集器
Serial 收集器它算是最古老,也是最基础的新生代垃圾收集器,基于标记-复制算法实现,同时它是工作在单线程模式下,但这里单线程模式需要注意下,它不仅仅是说明它只是采用单条线程来进行垃圾收集工作,同时也强调垃圾收集线程和用户的工作线程不能同时进行工作,垃圾线程工作时需要暂停用户的工作线程,这也就是臭名昭著的“Stop The World”状态,而后来的垃圾收集器也一直在为缩短这个状态的停留时间而努力优化改进。
正因为 Serial 收集器是单线程工作,实现简单,不需要复杂的数据结构来存储额外的数据,因此它也一直是工作在 Client 模式下的 JVM 垃圾收集器的首选项。
Parallel New 收集器
Parallel New 收集器是 Serial 收集器的多线程版,也就是说它采用多线程的方式来完成垃圾收集工作,但不可避免的在垃圾收集线程工作时还是要暂停用户的工作线程,其他的则和 Serial 收集器差不多,也是新生代收集器,基于标记-复制算法实现。常见的应用场景是配合老年代的 CMS 收集器一起工作,但随着垃圾收集器的不断优化迭代,更为优秀的垃圾收集器 G1 出现之后,它打乱了堆中新生代和老年代的划分结构,因此自从 G1 出现之后,Parallel New 收集器也就开始慢慢退出历史的舞台了。
Parallel Scavenge 收集器
Parallel Scavenge 收集器则和 Parallel New 收集器差不多,都是新生代收集器,使用多线程的方式完成垃圾收集工作,基于标记-复制算法实现,但它特殊的地方在于它更加注重吞吐率,这里的吞吐率表示的是用户线程工作时长与垃圾收集线程工作时长加上用户线程工作时长之和的比值,Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,一个是控制最大垃圾收集停顿时间 -XX: MaxGCPauseMillis,还一个是直接设置垃圾收集时间占总时间的比值 -XX:GCTimeRatio,前者是一个大于 0 的毫秒数,当然 JVM 也只是会尽力去控制这个垃圾收集时间接近于设置的值,后者是一个大于 0 小于 100 的整数百分比,相当于吞吐率的倒数。
老年代收集器
Serial Old,Parallel Old 收集器
Serial Old 收集器是 Serial 收集器的老年代版本,同样是单线程的,但它是基于标记-整理算法实现。
Parallel Old 收集器则是 Parallel Scavenge 收集器的老年代版本,采用多线程的方式进行收集,基于标记-整理算法实现。
CMS 收集器
CMS 收集器是基于标记-清除算法实现,并且是并发的,目标是尽量减少停顿时间,它只有少数几个操作需要 Stop The World,大部分是可以在用户程序运行过程中并发进行垃圾回收,当并发收集失败之后则会使用上面两个整理型垃圾回收器进行回收。
由于是基于标记-清除算法实现,这意味着会存在内存碎片问题,因此在长时间运行之后会产生 Full GC,既然发生了 Full GC 自然就会导致更长时间的停顿。而且由于是并发的,那么在并发收集的过程中也就不可避免会存在垃圾收集线程和用户工作线程互相竞争资源的情况,这些都是 CMS 收集器的比较明显的缺陷。
更多优秀收集器
G1(Garbage First)
G1 收集器是有点打破了之前将整个堆区按照分代区域划分的常规做法,它是以 Region 为单位将堆区划分为一个一个的 Region,然后每个 Region 都可以作为 Eden 区,Survivor 区或老年代,Region 之间是基于标记-复制算法实现,而整体上则是采用标记-整理的算法,可以有效避免内存碎片。在进行垃圾回收时,以 Region 来作为最小回收单元,当然不会每次都对全部的 Region 进行回收,而是对最有回收价值的那些 Region 区域进行回收,也就是优先回收死亡对象较多的区域,这样才能获得更好的回收效果,同时这也是为什么收集器的名称要叫 Garbage First。而且随着 G1 收集器的到来,CMS 收集器也开始逐渐被舍弃,在 JDK9 里面已经被标记为废弃了。
随着 JVM 的优化迭代,慢慢的已经有了更多更为优秀的垃圾收集器涌现出来,比如说 Shenandoah GC,ZGC,Epsilon GC,说实话这些收集器我也没有仔细去看过他们的特性,只是听说它们在进行垃圾收集时延迟可以做到更低,尤其是对于目前随着硬件配置越来越高的情况下,内存占用以及吞吐量都可以直接得到优化,但对于延迟的影响却是相反的,内存越大,垃圾收集占用的时间越长,同时这也会间接影响到用户线程的正常工作,因此目前对于低延迟的垃圾收集器更是迫在眉睫。
总结
对于上面提到的这些收集器的工作方式我只是做了个大概的介绍,更多的细节我建议还是去仔细看看 《深入理解 Java 虚拟机》这本书,当然最终的目的是根据这些收集器的特征,然后结合我们的实际应用,给这些收集器配置合理的参数,来达到我们想要的优化效果。可能上面这些描述你看起来觉得很简单,但其实每个收集器后面的实现细节都是非常复杂的,尤其是后面不断涌现出来的收集器,它们的工作方式以及实现细节更是复杂,可能我们说起来就是那么几句话,但背后的细节原理还是需要花好些时间去深入研究的,毕竟每个收集器都是经历过那么多的服务器实践优化之后才正式进入我们的视野,感兴趣的话可以去深入研究这些细节。