一些术语:
-
STW:Stop The World,指GC回收时暂停所有用户线程的现象
-
并发:指GC线程与用户线程并发执行,不会产生STW
-
并行:指GC线程是多线程并行执行,会产生STW
GC算法
标记-清除算法
分为标记和清除两个阶段(Mark-Sweep),这个算法是最基础的收集算法,先标记处所有需要回收的对象,标记完成后统一回收。主要存在的不足之处有两点
-
效率问题:标记和清除两个过程的效率都不高
-
空间问题:被标记回收的对象所在的内存位置是不连续的,因此在清理之后剩余空间也是不连续的,会产生空间碎片,导致后续不能分配连续空间而提前触发另一次垃圾回收
执行过程的示意图如下:
复制算法
复制算法主要是将内存划分为容量相等的两块,每次只使用一块,需要回收的时候,将还需要存活的对象复制到另外一块内存当中,然后把原使用的那块内存直接清理掉,根据这种算法的方式,可以分析出其优缺点
优点:
-
复制的时候是将对象挨个放入到空的内存块中,所以不会有空间碎片的问题
-
实现起来较为简单,运行起来比较高效
缺点:
- 内存空间的利用率不大,因为始终要保持一半内存的空闲状态
适用于低内存空间(因为利用率只有50%)、存活对象较少(复制效率会更高)的场景
标记-整理(-清除)算法
总体上与标记-清除算法一样,只是多了一个内存整理的过程,主要是为了解决上面提到的标记-清除算法引起的空间碎片的问题
分代收集算法
分代收集算法并没有新的算法收集方式,只是将内存划分为新生代和老年代,对不同分代采取不同的垃圾收集算法,综合不同分代的特性以提高垃圾收集的效率。比如年轻代的特点是对象存活率低,即大量对象都会被回收,因此比较适合采用复制算法;而老年代的特点是对象存活率高,,所以比较适合采用标记-清理或标记-整理算法
GC策略
Serial
Serial收集器是发展最久的垃圾回收器,Serial单次本身的意思是“串行”,是一个单线程的收集器,作用于新生代,采用复制算法,在进行垃圾回收的时候会产生STW,主要适用于单核CPU的场景,可以避免多线程上下文切换带来的消耗
对于Serial收集器有个疑问,在《深入理解Java虚拟机-JVM高级特性与最佳实践》第2版第76页有如下描述:
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。大家看名字就会知道,这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
这里描述的Serial可能并不是只有一个垃圾收集线程,也就是说明可能是多线程,但是在ParNew收集器中又说ParNew是Serial收集器的多线程版本,感觉两处有些矛盾,因为没看过源码,所以不知道具体是怎样的情况。如有知情者请告知,万分感谢
Serial Old
Serial的老年代版本,即与Serial一样,采用单线程垃圾回收,不同的是采用的标记-整理算法
ParNew
ParNew是Serial的多线程版本,即使用多线程进行垃圾回收,作用于新生代,采用复制算法,进行垃圾回收时会产生STW
Parallel Scavenge
Parallel Scavenge与ParNew类似,只是关注点不同。Parallel Scavenge主要关注吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。提供了两个参数来控制吞吐量
-
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间,大于0的毫秒数
-
-XX:GCTimeRatio 设置吞吐量大小,大于0且小于100的整数,即垃圾收集时间占总时间的比率
Parallel Old
Parallel Old是Parallel Scavenge的老年代版本,多线程收集,使用标记-整理算法
CMS
以获取最短回收停顿时间为目标,作用于老年代,使用标记-清除算法,会产生空间碎片,但可以通过配置 -XX:+UseCmsCompactAtFullCollection (默认已开启)用于在CMS收集器要进行FullGC时开启内存碎片的整理
标记-清除的过程如下:
-
初始标记:只标记GC Roots直接关联到的对象,速度快,会产生STW
-
并发标记:根据GC Roots标记的对象进行追踪,标记出需要回收的对象。过程较长,但GC线程与用户线程并发执行,不产生 STW
-
重新标记:修复并发标记过程中用户程序的运行导致标记的对象产生变动的的那部分对象,会产生STW
-
并发清除:清除标记的对象,与用户线程并发执行,不产生STW
G1
G1回收器除了综合其他回收器的回收方法之外,还将Java堆内存划分为很多个大小相等的独立区域Region,对新生代老年代也没有物理上的划分了,都是多个Region的集合。因此G1是作用在新生代和老年代的。从整体上看是使用的标记-整理算法,也避免了空间碎片的问题,从Region局部来看也有复制算法
-
初始标记:只标记GC Roots直接关联到的对象,速度快,会产生STW
-
并发标记:根据GC Roots标记的对象进行追踪,标记出需要回收的对象。过程较长,但GC线程与用户线程并发执行,不产生 STW
-
最终标记:与CMS的重新标记类似,都是为了修复并发标记过程中用户程序的运行导致的变动的对象
-
筛选回收:对各个Region的回收价值和回收成本进行排序,根据配置的期望GC停顿时间来决定回收计划,会产生STW,但也可以做到与用户程序并发执行
总结
GC回收器总结
回收器名称 | 使用算法 | 并发/并行 | 适用CPU场景 | 新生代 | 老年代 | STW | 其他 |
---|---|---|---|---|---|---|---|
Serial | 复制算法 | 并行 | 单核 | √ | × | 整个GC过程会STW | |
Serial Old | 标记-整理算法 | 并行 | 单核 | × | √ | 整个GC过程会STW | 此回收器同时也是CMS的备用回收器 |
ParNew | 复制算法 | 并行 | 多核 | √ | × | 整个GC过程会STW | Serial的多线程版本,有很大部分公用代码 |
Parallel Scavenge | 复制算法 | 并行 | 多核 | √ | × | 整个GC过程会STW | 与ParNew类似,只是更关注吞吐量,可通过配置控制吞吐量 |
Parallel Old | 标记-整理算法 | 并行 | 多核 | × | √ | 整个GC过程会STW | Parallel Scavenge的老年代版本 |
CMS | 标记-清除算法 | 并发+并行 | 多核 | × | √ | 初始标记和重新标记会STW | 共3次标记,需要注意空间碎片。回收失败后会采用Serial Old备用回收器 |
G1 | 标记-整理算法、复制算法 | 并发+并行 | 多核 | √ | √ | 初始标记和最终标记会STW | 共3次标记,多Region,可通过配置控制吞吐量 |
GC组合总结
根据前面的介绍可以看出:
-
新生代收集器:Serial、ParNew、Parallel Scavenge
-
老年代收集器:Serial Old、Parallel Old、CMS
-
整堆收集器:G1
可能的组合情况如下:
各个组合说明如下:
新生代策略 | 老年代策略 | 说明 |
---|---|---|
Serial | Serial Old | Serial和Serial Old都是单线程进行GC,会产生STW,适用于单核场景 |
Serial | CMS + Serial Old | 当CMS进行GC失败时,会自动使用Serial Old策略进行GC |
ParNew | CMS + Serial Old | 如果指定CMS回收器,则新生代会默认使用ParNew回收策略 |
ParNew | Serial Old | -XX:+UseParNewGC开启,新生代使用ParNew回收器时,老年代默认使用Serial Old回收策略 |
Parallel Scavenge | Serial Old | Parallel Scavenge适用于后台持久运行的应用程序,如跑批应用 |
Parallel Scavenge | Parallel Old | 两者都是并行回收策略 |
G1 | G1 | 需要留意G1的稳定性 |
参考资料
-
《深入理解Java虚拟机-JVM高级特性与最佳实践》第2版
文中截图均来自《深入理解Java虚拟机-JVM高级特性与最佳实践》第2版