JAVA Hotspot虚拟机经典垃圾收集器的优点缺点简单总结
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8686dee20b73b971de357cb4f0f475b5.png)
图中展示了七种作用于不同分代的收集器, 如果两个收集器之间存在连线, 就说明它们可以搭配使用, 图中收集器所处的区域, 则表示它是属于新生代收集器抑或是老年代收集器。
Serial 收集器
一个单线程工作的收集器, 但它的 “单线程” 的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作, 更重要的是强调在它进行垃圾收集时, 必须暂停其他所有工作线程(Stop the world), 直到它收集结束。迄今为止, 它依然是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器。
优点:
- 简单而高效(与其他收集器的单线程相比), 对于内存资源受限的环境, 它是所有收集器里额外内存消耗( Memory Footprint) 最小的;
- 对于单核处理器或处理器核心数较少的环境来说, Serial 收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率。
缺点: 单线程的垃圾收集器,在进行垃圾收集时会导致STW。
ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本。ParNew 收集器的工作过程如图所示:
ParNew 收集器除了支持多线程并行收集之外, 其他与 Serial 收集器相比并没有太多创新之处, 但它却是不少运行在服务端模式下的 HotSpot 虚拟机, 尤其是 JDK 7 之前的遗留系统中首选的新生代收集器, 其中有一个与功能 、 性能无关但其实很重要的原因是:除了Serial 收集器外, 目前只有它能与 CMS 收集器配合工作。
特点:
- 多线程版本的Serial收集器。
- ParNew与CMS垃圾收集器进行配合使用。
- 在单核心处理器下比不过Serial收集器。随着可以被使用的处理器核心数量的增加, ParNew 对于垃圾收集时系统资源的高效利用还是很有好处的。
- ParNew也会产生STW。
Parallel Scavenge 收集器(吞吐量优先收集器)
Parallel Scavenge 收集器也是一款新生代收集器, 它同样是基于标记 - 复制算法实现的收集器, 也是能够并行收集的多线程收集器。Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。 所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
Parallel Scavenge 收集器也经常被称作 “吞吐量优先收集器” 。
特点: Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同, CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 它同样是一个单线程收集器, 使用标记 - 整理算法。 这个收集器的主要意义也是供客户端模式下的 HotSpot 虚拟机使用。 如果在服务端模式下, 它也可能有两种用途: 一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用, 另外一种就是作为 CMS 收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure 时使用。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 支持多线程并发收集, 基于标记 - 整理算法实现。
这个收集器是直到 JDK 6 时才开始提供的, 在此之前, 新生代的 Parallel Scavenge 收集器一直处于相当尴尬的状态, 原因是如果新生代选择了 Parallel Scavenge 收集器, 老年代除了 Serial Old ( PS MarkSweep ) 收集器以外别无选择, 其他表现
良好的老年代收集器, 如 CMS 无法与它配合工作。 由于老年代 Serial Old 收集器在服务端应用性能上的 “拖累” , 使用 Parallel Scavenge 收集器也未必能在整体上获得吞吐量最大化的效果。 同样, 由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中, 这种组合的总吞吐量甚至不
一定比 ParNew 加 CMS 的组合来得优秀。
直到 Parallel Old 收集器出现后, “吞吐量优先” 收集器终于有了比较名副其实的搭配组合, 在注重吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑 Parallel Scavenge 加Parallel Old 收集器这个组合。 Parallel Old 收集器的工作过程如图所示:
特点:Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 支持多线程并发收集, 基于标记 - 整理算法实现。
Parallel Scavenge与Parallel Old组合使用。
CMS 收集器
CMS ( Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
目前很大一部分的 Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上, 这类应用通常都会较为关注服务的响应速度, 希望系统停顿时间尽可能短, 以给用户带来良好的交互体验。 CMS 收集器就非常符合这类应用的需求。
整个过程分为四个步骤:
- 初始标记(CMS initial mark)
初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,需要STW,但是速度很快; - 并发标记(CMS concurrent mark)
并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行; - 重新标记 ( CMS remark )
重新标记阶段则是为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短;需要STW。 - 并发清除(CMS concurrent sweep)
并发清除阶段, 清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并发清除阶段中, 垃圾收集器线程都可以与用户线程一起工作, 所以从总体上来说, CMS 收集器的内存回收过程是与用户线程一起并发执行的。 其执行过程如图所示:
优点:
- 并发收集、低停顿。
缺点:
- CMS 收集器对处理器资源非常敏感。 事实上, 面向并发设计的程序都对处理器资源比较敏感。 在并发阶段, 它虽然不会导致用户线程停顿, 但却会因为占用了一部分线程( 或者说处理器的计算能力) 而导致应用程序变慢, 降低总吞吐量。
- CMS 收集器无法处理 “浮动垃圾”( Floating Garbage), 有可能出现 “Concurrent Mode Failure” 失败进而导致另一次完全 “ Stop The World” 的 Full GC 的产生。在 CMS 的并发标记和并发清理阶段, 用户线程是还在继续运行的, 程序在运行自然就还会伴随有新的垃圾对象不断产生, 但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。 这一部分垃圾就称为 “浮动垃圾” 。 同样也是由于在垃圾收集阶段用户线程还需要持续运行, 那就还需要预留足够内存空间提供给用户线程使用, 因此 CMS 收集器不能像其他收集器那样等
待到老年代几乎完全被填满了再进行收集, 必须预留一部分空间供并发收集时的程序运作使用。要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要, 就会出现一次 “并发失败”( Concurrent Mode Failure), 这时候虚拟机将不得不启
动后备预案: 冻结用户线程的执行, 临时启用 Serial Old 收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。 - 产生内存碎片。CMS 是一款基于 “标记 - 清除” 算法实现的收集器, 如果读者对前面这部分介绍还有印象的话, 就可能想到这意味着收集结束时会有大量空间碎片产生。 空间碎片过多时, 将会给大对象分配带来很大麻烦, 往往会出现老
年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次 Full GC 的情况。
Garbage First 收集器
Garbage First (简称 G1 ) 收集器是垃圾收集器技术发展历史上的里程碑式的成果,开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。
G1 是一款主要面向服务端应用的垃圾收集器。 JDK 9 发布之日, G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合, 成为服务端模式下的默认垃圾收集器, 而 CMS 则沦落至被声明为不推荐使用( Deprecate) 的收集器。
G1 满足 “停顿时间模型”( Pause Prediction Model ) 的收集器, 停顿时间模型的意思是能够支持指定在一个长度为M 毫秒的时间片段内, 消耗在垃圾收集上的时间大概率不超过 N 毫秒这样的目标.
G1 可以面向堆内存任何部分来组成回收集 (Collection Set, 一般简称 CSet) 进行回收, 衡量标准不再是它属于哪个分代, 而是哪块内存中存放的垃圾数量最多, 回收收益最大, 这就是 G1 收集器的 Mixed GC 模式。
G1 开创的基于 Region 的堆内存布局是它能够实现这个目标的关键。 虽然 G1 也仍是遵循分代收集理论设计的, 但其堆内存的布局与其他收集器有非常明显的差异: G1 不再坚持固定大小以及固定数量的分代区域划分, 而是把连续的 Java 堆划分为多个大小相等的独立区域( Region), 每一个 Region 都可以根据需要, 扮演新生代的 Eden 空间、 Survivor 空间,或者老年代空间。 收集器能够对扮演不同角色的 Region 采用不同的策略去处理, 这样无论是新创建的对象还是已经存活了一段时间、 熬过多次收集的旧对象都能获取很好的收集效果。
Region 中还有一类特殊的 Humongous 区域, 专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。
虽然 G1 仍然保留新生代和老年代的概念, 但新生代和老年代不再是固定的了, 它们都是一系列区域(不需要连续) 的动态集合。 G1 收集器之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元, 即每次收集到的内存空间都是 Region 大小的整数倍, 这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集, 思路是让 G1 收集器去跟踪各个 Region 里面的垃圾堆积的 “价值” 大小, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的收集停顿时间( 使用参数-XX:MaxGCPauseMillis 指定, 默认值是 200 毫秒),优先处理回收价值收益最大的那些 Region, 这也就是 “ Garbage First” 名字的由来。 这种使用 Region 划分内存空间, 以及具有优先级的区域回收方式, 保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。
G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记( Initial Marking): 仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS (程序与回收线程要同时运行就肯定会持续有新对象被创建, G1 为每一个 Region 设计了两个名为 TAMS ( Top at Mark Start) 的指针, 把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配, 并发回收时新分配的对象地址都必须要在这两个指针位置以上。 G1 收集器默认在这个地址以上的对象是被隐式标记过的, 即默认它们是存活的, 不纳入回收范围。 与 CMS 中的 “ ConcurrentMode Failure” 失败会导致 Full GC 类似, 如果内存回收的速度赶不上内存分配的速度, G1 收集器也要被迫冻结用户线程执行, 导致 Full GC 而产生长时间 “ Stop The World")指针的值, 让下一阶段用户线程并发运行时, 能正确地在可用的 Region 中分配新对象。 这个阶段需要停顿线程, 但耗时很短,** 而且是借用进行Minor GC 的时候同步完成的, 所以 G1 收集器在这个阶段实际并没有额外的停顿。
- 并发标记( Concurrent Marking): 从 GC Root 开始对堆中对象进行可达性分析, 递归扫描整个堆里的对象图, 找出要回收的对象, 这阶段耗时较长, 但可与用户程序并发执行。 当对象图扫描完成以后, 还要重新处理 SATB (Snapshot At
The Beginning,原始快照算法,用来解决三色标记法中赋值器删除了全部从灰色对象到该白色对象的直接或间接引用的方法)记录下的在并发时有引用变动的对象。 - 最终标记 ( Final Marking):对用户线程做另一个短暂的暂停, 用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
- 筛 选 回 收( Live Data Counting and Evacuation): 负责更新 Region 的统计数据, 对各个 Region 的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个 Region 构成回收集, 然后把决定回收的那一部分Region 的存活对象复制到空的 Region 中, 再清理掉整个旧 Region 的全部空间。 这里的操作涉及存活对象的移动, 是必须暂停用户线程, 由多条收集器线程并行完成的。
G1 收集器除了并发标记外, 其余阶段也是要完全暂停用户线程的, 换言之, 它并非纯粹地追求低延迟, 官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量, 所以才能担当起 “全功能收集器” 的重任与期望。
G1 收集器的运作步骤中并发和需要停顿的阶段如图所示:
可以由用户指定期望的停顿时间是 G1 收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得 G1 在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 但是也不能设置太短,如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发 Full GC 反而降低性能。
优点:
-
可以指定最大停顿时间、 分 Region 的内存布局、 按收益动态确定回收集。
-
Mixed GC模式。可以面向堆内存任何部分来组成回收集进行回收, 衡量标准不再是它属于哪个分代, 而是哪块内存中存放的垃圾数量最多, 回收收益最大, 这就是 G1 收集器的 Mixed GC 模式。
-
G1 将 Region 作为单次回收的最小单元, 即每次收集到的内存空间都是 Region 大小的整数倍, 这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。
-
不会产生内碎片。与 CMS 的 “标记 - 清除” 算法不同, G1 从整体来看是基于 “标记-整理” 算法实现的收集器, 但从局部( 两个 Region 之间) 上看又是基于 “标记- 复制” 算法实现, 无论如何, 这两种算法都意味着 G1 运作期间不会产生内存空间碎片, 垃圾收集完成之后能提供规整的可用内存。 这种特性有利于程序长时间运行, 在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
-
具有优先级的区域回收;用户可以设置收集停顿时间,优先处理回收价值收益最大的那些 Region, 式, 保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。
缺点:
-
在用户程序运行过程中, G1 无论是为了垃圾收集产生的内存占用(Footprint) 还是程序运行时的额外执行负载 (Overload) 都要比 CMS 要高。
-
在小内存应用上 CMS 的表现大概率仍然要会优于 G1 , 而在大内存应用上 G1 则大多能发挥其优势, 这个优劣势的 Java 堆容量平衡点通常在 6GB 至8GB 之间,
参考:《深入理解Java虚拟机第三版 周志明》