【JVM系列】Java收集器中年迈老大哥------Serial和Serial Old收集器
欢迎关注,分享更多原创技术内容~
微信公众号:ByteRaccoon、知乎:一只大狸花啊、稀土掘金:浣熊say
微信公众号海量Java、数字孪生、工业互联网电子书免费送~
Serial收集器
垃圾回收与仓库清理------"Stop The World"
在前面的文章中,说过Java中对堆内存的管理就像是对一个大型的仓库进行管理一样。我们想要清扫这个大仓库中无用的设备的话,那么必须确保能正确识别哪些设备还有用哪些设备已经无用了,并且这个过程中不允许新的设备入库和出库。等到标记完成之后,再进行设备清理才会保证不会出现把本来还有用的设备当成垃圾处理掉。可以试想一下,如果在清理仓库的过程中不禁止出库和入库,那么整个仓库的管理就会陷入混乱,随时可能会产生新的无用设备(浮动垃圾),并且还可能会出现错误的清理有用的设备(漏标)。
所以回到垃圾回收上,几乎所有的垃圾回收器都需要在垃圾回收阶段暂停用户线程,来确保垃圾被正确的标记和清理,这一停顿的过程被称为"Stop The World"(停顿整个世界)。这种停顿在确保堆内存被完整清理的同时也给用户带来了恶劣的体验,在停顿期间,应用程序无法响应用户的请求,导致了性能和用户体验的下降。
那么为什么非得STW暂停用户线程呢?上面用仓库举例只能说明如果在清理仓库的时候不禁止出库入库会造成一些混乱,实际上这些混乱主要有2个方面的问题------漏标和多标(当然,像Serial和PS这种完全STW的算法从根本上就避免了这两个问题)。一般的垃圾回收算法在垃圾标记阶段都是采用的"三色标记算法",这种算法如果在标记过程中不暂停用户线程的话就可能会出现"漏标"和"多标"的问题,他们会分别导致"存活对象被回收"和"浮动垃圾"。尤其是"漏标"导致的存活对象被回收之后,就会导致严重的系统异常,进而导致程序崩溃。
所以,无论什么垃圾回收算法,都需要一定时间的STW来正确的标记对象。现在主流的垃圾收集器一样需要停顿,如CMS、G1和ZGC等垃圾回收器也并没有完全避免STW,只是都做到了毫秒级的停顿,可以说STW是垃圾回收中一个不可避免的问题。
Serial垃圾回收器(年轻代)
Serial垃圾收集器,顾名思义是一种单线程的"串行的"垃圾回收器,是历史最悠久的垃圾回收器,大概在JDK 1.3之前Serial垃圾收集器是HotSpot虚拟机新生代垃圾回收的唯一选择。时至今日,虽然各种垃圾回收器层出不穷,也非常先进,但是我们仍然可以指定Serial作为垃圾回收器的选项,只是一般不建议这样做而已。
Serial收集器是单线程的,当Serial垃圾回收器运行的时候其它工作线程必须停止直到Serial垃圾回收器结束工作("Stop The World")。用户线程的状态会被保存在一个SafePoint的状态中,此时,GC线程以单线程的形式对垃圾进行标记(根可达算法)和回收(复制算法)。
使用Serial垃圾回收器的命令行参数是-XX:+UseSerialGC
。这个参数告诉Java虚拟机在运行时使用Serial垃圾回收器。如果你想在Java应用程序中显式地设置Serial垃圾回收器,可以通过以下方式运行你的Java程序:
bash
java -XX:+UseSerialGC -jar YourApplication.jar
Serial垃圾回收器采用单线程最明显的缺点就是需要"Stop The World",暂停工作线程的工作,对应的服务处理也会暂停,影响程序的的性能,所以使用Serial的单线程的垃圾回收模式尝尝造成很长的停顿时间,一般不适用于对性能要求高的后端服务。
但是单线程模式也有优点,那就是可以减少上下文切换,减少系统资源的开销,对于区域比较小的新生代来说造成的停顿时间较短。因此,Serial垃圾回收器通常适用于较小规模的应用或者特定场景,对于大型服务器端应用,通常会考虑使用更高级的垃圾回收器。
Serial Old收集器(老年代)
Serial Old收集器是 Serial 收集器的老年代版本,其基本的实现原理和Serial垃圾回收器一样,都是使用单线程串行的方式进行垃圾收集。但是由于是回收清理的老年代的内存区域,区域较大,因此回收算法采用的是"标记-整理"算法,这样能够有效的避免老年代区域的内存碎片,提升内存空间利用率。
由于采用单线程的模式,Serial Old同样需要"Stop-The-World",从而造成长时间的用户线程停顿。照理说这样效率低下的单线程的垃圾回收器到现如今应该已经被淘汰的差不多了,尤其是对于现如今动辄几十个G的堆内存空间而言。但是,有趣的是Serial Old垃圾回收器还是广泛的被用在现如今的JVM当中,但是一般不是作为主力的老年代垃圾回收器来使用,而是作为CMS、G1等垃圾回收器的降级策略来使用。
对于CMS和G1等并发的垃圾回收器,由于垃圾清理阶段是并发执行的,始终会存在"浮动垃圾"无法被清理,这样以来在某些情况下可能会造成内存分配失败的情况。作为一种降级策略,Serial Old会停止所有的用户线程,并对全堆的内存空间进行垃圾回收。在如下表所示的2种情况下会使用Serial Old垃圾回收器来作为降级策略:
垃圾回收器 | 异常 | 原因 |
---|---|---|
CMS(Concurrent Mark-Sweep) | Concurrent Mode Failure | 在CMS中浮动垃圾的产生可能会造成垃圾并发清理过程中无法分配对象的问题。CMS会临时启用Serial Old来进行一次Full GC |
G1(Garbage First) | 担保机制 | 当G1在堆空间无法分配新的Region时,G1会启动担保机制,Serial Old单线程的Full GC |
可以这样说,Serial Old垃圾回收器是回收得最干净的垃圾回收器,回收过程中也不会产生浮动垃圾,所以G1和CMS垃圾回收器在才会选用Serial Old来作为降级策略。但是由于其单线程和长时间停顿用户线程的特点,对于当今动辄几十个G的内存空间来说,其回收时间是灾难性的。因此,在现代的垃圾回收器中,要尽量避免类似于CMS、G1等垃圾回收器降级成为Serial Old垃圾回收器。
在 Java 虚拟机启动时,可以通过设置不同的垃圾收集器参数来选择使用 Serial 或 Serial Old 垃圾收集器。以下是使用这两者的组合,即在整个堆上使用串行垃圾回收的 Java 虚拟机启动命令的示例:
bash
java -XX:+UseSerialGC -XX:+UseSerialOldGC -jar YourApplication.jar
这样的配置会使得新生代和老年代都使用串行垃圾收集器。这种配置通常用于小型应用或测试环境,因为串行垃圾收集器在大型、高并发应用中可能导致较长的停顿时间。
JVM相关参数------Serial Old垃圾回收器
参数 | 描述 |
---|---|
-XX:+UseSerialGC | 启用Serial垃圾收集器。 |
-XX:+UseSerialOldGC | 启用Serial Old垃圾收集器。 |
-XX:SurvivorRatio=n | 设置新生代中Eden区域与Survivor区域的比例。默认值为8,表示Eden占整个新生代的8/10,每个Survivor区占1/10。 |
-XX:NewRatio=n | 设置新生代与老年代的比例。默认值为2,表示新生代占整个堆的1/3,老年代占2/3。 |
-XX:MaxTenuringThreshold=n | 设置对象从新生代晋升到老年代的最大年龄。默认值为15。 |
-XX:TargetSurvivorRatio=n | 设置新生代中Survivor区域的目标使用率。默认值为50。 |
-XX:PretenureSizeThreshold=n | 设置大对象直接进入老年代的阈值。默认值为0,表示大对象直接在新生代分配。 |
-XX:UseTLAB | 启用线程本地分配缓冲(TLAB)。默认值为true。 |
-XX:TLABSize=n | 设置TLAB的大小。默认值为0,表示根据堆的大小动态调整。 |
-XX:PrintGCDetails | 打印详细的垃圾回收日志。 |
以上这些参数可以通过在Java虚拟机启动命令中使用-XX:
的形式进行设置,来调整Serial垃圾回收器的性能,但是,在生产环境最好不要使用Serial 垃圾回收器。
总结
Serial垃圾回收器几乎是HotSpot虚拟机中最古老的垃圾回收器,其采用了单线程模型和串行的模式,可以对年轻代和老年代进行垃圾回收。但是,由于单线程的特点,其效率较低,不适合作为大型的后台服务程序来进行使用,而一般被用在G1和CMS等垃圾回收器的降级策略当中。