JVM垃圾收集器
如果说垃圾收集算法是方法论,那么垃圾收集器就是内存回收的实践者了,让我们从远到近来具体了解一下JVM中的垃圾收集器
Serial
概述:最基础和远古的收集器
缺点:单线程工作 (会把直接暂停程序其他所有工作线程的那种单线程)
优点:简单高效,所有收集器里额外内存消耗最小,因为没有多线程交互开支,所以单线程效率最高。适合核心或者线程数较少的环境。
应用:JDK1.3.1 之前HotSpot虚拟机新生代收集器的唯一选择。至今的HotSpot客户端环境下的新生代收集器。可能微服务场景也是个不错的选择。
ParNew
概述:简单来说就是Serial的多线程并行版本,垃圾收集算法,Stop The World,对象分配规则,回收策略和Serial都是一样的,代码共同部分也是相当多。
缺点:总体和Serial差不多,无法和用户线程同时工作。核心数量少的情况下,因为有线程交互的开销,效率还不及Serial.
优点:除了Serial之外唯一可以和CMS配合使用的收集器。多核心环境下效率尚可。
应用:在JDK8及之前,和CMS配合使用作为新生代收集器,基本上可以理解为并入了CMS中。
Parallel Scavenge
概述:也是一款新生代收集器,也是基于标记复制算法实现的收集器,也可以并行收集,和ParNew有点像。
缺点:和ParNew差不多,然后因为是吞吐量优先,所以用户体验可能不太好。
优点:吞吐量优先,适合后台运算不需要太多交互的场景。只需要提供基础的一些参数,就可以做到自适应调节策略。
应用:对计算型的交互少的应用场景比较适合。
Serial Old
概述:Serial 的老年代版本,同样是一个单线程收集器,使用标记-整理算法
应用:在JDK5以前和Parallel Scavenge搭配使用,作为CMS发生失败后的备选方案
Parallel Old
概述:Parallel Scavenge的老年代版本,这个收集器到JDK6才开始提供,导致Parallel Scavenge在之前就很尴尬
应用:和Parallel Scavenge搭配,让吞吐量优先的收集器终于有了名副其实的选择
CMS
概述:(Concurrent Mark Sweep)CMS收集器是一种以获得最短回收停顿时间为目标的收集器
基于标记-清除法实现,运作相较之前的收集器稍显复杂分四个步骤
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
初始标记和重新标记还是需要 Stop The World - 初始标记只是标记一下GC Root 能够关联到的对象,耗时很短
- 并发标记就是从GC Root 直接关联对象开始遍历整个对象图,耗时较长,但不需要停顿用户线程,可以与垃圾收集线程并发运行
- 重新标记阶段就是为了修正并发标记期间因为程序运行而导致标记产生变动的那一部分对象的标记记录,这个阶段耗时比通常比初始标记稍长
- 并发清除阶段就是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以可以和用户线程同时工作
优点:并发收集,低停顿
缺点: - 并发阶段会占用系统的线程资源导致程序运行变慢,降低总吞吐量,在CPU核心数量较少的场景下较为明显
- 无法处理“浮动垃圾”,也就是在标记过程结束之后程序运行产生的垃圾,这一部分垃圾无法在本次垃圾回收中被处理,所以CMS不会在老年代几乎被填满的时候才激活,它会预留一部分空间供并发收集时的程序运作,这个阈值在JDK5是68%,在JDK6是92%,提高这个阈值可以降低垃圾收集的频率,但会带来另一个风险,那就是CMS预留空间无法满足程序运行,那么就会出现一次并发失败(Concurrent Mode Failure),这时就会冻结用户线程,临时采用Serial Old 来对老年代进行垃圾收集。
调整 -XX:CMSInitiatingOccu-pancyFraction 的值可以调整这个阈值
- 因为基于标记-清除算法,就意味着收集结束之后可能会产生大量空间碎片,给大对象的分配带来麻烦,如果无法分配,那么就会触发一次Full GC,这里CMS提供了两个开关参数(以下两参数在JDK9都被废弃)
-XX:+UseCMS-CompactAtFullCollection 默认开启意味着进行Full GC时就会进行空间整理
-XX:CMSFullGCsBefore-Compaction 默认值为0意味着每次Full GC之后都会进行空间整理
Garbage First 收集器
概述:
- 里程碑式的收集器,开创了面向局部收集的设计思路和基于Region的内存布局。每个Region可以根据需要扮演新生代的Eden或者Survivor,或者老年代
Region中还有一类Humongous区域用于储存大对象,大对象就是超过Region一半大小的对象,Region 大小可以通过 -XX:G1HeapRegionSize 来指定,而对于超级大对象,它会被存放在N个连续的Humongous Region中
- 主要面向服务端应用程序,HotSpot团队给它制定的目标就是替换掉CMS,JDK9取代之前提到的Parallel Scavenge+Parallel old 成为服务端模式下默认的垃圾收集器
- 拥有“停顿预测模型”,它不再需要对整个新生代或者老年代又或者Java堆进行垃圾回收,基于Region,它可以面向堆内存的任何部分来组成回收集(Collection Set),衡量标准不再是它属于哪个分代,而是回收收益最大化
G1 实现停顿预测是通过维护一个优先队列,通过控制每次收集的Region数量,优先处理回收价值大的Region 就可以基本实现指定的停顿时间。-XX:MaxGCPauseMillis指定,默认值是200ms
步骤:
- 初始标记(Inital Marking):仅标记GC Root能直接关联到的对象,同时修改TAMS(Top at Mark Start)指针,耗时很短,且可以在MinorGC的同步完成,所以没有额外停顿时间。
- 并发标记(Concurrent Marking): 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆中的对象,找出需要回收的对象,耗时很长,但可以和用户线程并发执行。最终还需要处理STAB(原始快照)记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程进行短暂的停顿,处理并发过程中遗留下来的少量STAB记录。
- 筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户希望的停顿时间来指定回收计划。把决定回收的Region中的存活对象复制到空Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成
通过对停顿期望值的合理设置,可以让G1在吞吐量和低延迟之间取得最佳的平衡
CMS与G1的对比
G1 优点:
- 可以指定最大停顿时间
- 分Region的内存布局
- 按收益动态确定回收集
- 整体基于标记-整理,局部Region基于标记-复制,意味着G1运行时不会产生空间碎片,有利于长时间运行
G1缺点:
- 垃圾收集的内存开销比CMS大
- 程序运行时的额外执行负载高于CMS
应用场景:
在小内存应用中CMS还是大概率要优于G1,而在大内存中G1表现更出色。这个内存阈值经验上来说是6~8GB。但这个还是得具体问题具体分析。