前言
如果说收集算法是内存回收的方法论, 那么垃圾收集器就是内存回收的具体实现. java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定, 因此不同的厂商, 不同的版本的虚拟机所提供的垃圾收集器都有可能会有很大的区别, 并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器.
相关系列博客:
深入理解 java 虚拟机之 java 内存区域
深入理解 java 虚拟机之对象真的死了吗
Java 垃圾收集算法
上图中展示了不同年龄代的收集器, 其中 Serial,ParNew 和 Parallel Scavenge 收集器作用于新生代, CMS,Parallel Old 和 Serial Old 作用于老年代, G1 在新生代和老年代都可以使用. 不同的收集器之间如果有连线, 则说明他们可以相互搭配使用.
相关概念
并行: 指的是多条垃圾收集线程一起公共, 但是此时用户工作线程仍处于等待状态.
并发: 指的是用户线程和垃圾收集线程同时工作, 也有可能是交替执行, 用户程序在继续执行, 而垃圾收集程序运行与另一个 CPU 上.
吞吐量: 吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值, 即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间). 虚拟机总共运行了 100 分钟, 其中垃圾收集花掉 1 分钟, 那吞吐量就是 99%.
Serial 收集器
Serial 收集器是一款串行执行的收集器, 它是历史最悠久, 也是最基本的收集器, 采用复制算法实现的新生代收集器. 在 jdk1.3 以前, Serial 收集器是新生代唯一的选择. 它是一个单线程执行的收集器, 工作时只会知用一个 CPU 或线程区执行, 更重要的是 Serial 在工作期间必须停掉所有的用户线程, 直至垃圾收集完成, 这一过程我们称之为 "stop the world". 这项工作是由虚拟机自动执行和自动完成的, 用户在不知情的情况下停掉了所有的线程, 这对于一个最求响应速度来说简直是无法接受的. 下图展示了 Serial 收集器在工作时的运行流程:
由于 Serial 收集器的工作模式是单线程的, 自然就没有了多线程环境下线程切换带来的性能开销, 所以该收集器在单线程环境下更加简单高效.
ParNew 收集器
Parnew 是 Serial 收集器的多线程版本, 也是新生代收集器. ParNew 收集器和 Serial 收集器除了多线程工作外几乎是相同的, 包括所有控制参数, 收集算法, stop the world, 对象分配规则, 回收策略等都是一样的. 运行流程如下图:
虽然与 Serial 收集器相比仅仅多了多线程特性外, 没有其它的创新之处, 但是它却是许多 Server 模式下的虚拟机新生代收集器的首选, 原因在于目前为止只有 Serial 和 ParNew 两个新生代收集器能够与性能优异的 CMS 配合使用. 关于 CMS 介绍将在下文展开描述.
Parallel Scavenge 收集器
Parallel Scavenge 也是一款使用复制算法的新生代收集器. 该收集器与其它收集器不同的是, 它关注的目标是达到一个可控制的吞吐量, 而 CMS 等收集器的关注点则是尽可能地减少用户线程地停顿时间, 提高用户体验. Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量, 分别是控制最大垃圾收集停顿时间的 - XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 - XX:GCTimeRatio 参数. 也因此, Parallel Scavenge 被成为 "吞吐量优先" 收集器.
停顿时间越短就越适合与用户交互较多地程序, 这样用户体验才更好. 而高吞吐量则可以让出更多的 CPU 资源给用户线程, 让程序更快的完成运算任务, 更适合后台运算较多而不需要与用户交互的程序.
自适应调节策略是 Parallel Scavenge 收集器的特点, 也是与 ParNew 收集器的区别. Parallel Scavenge 通过打开 - XX:+UseAdaptiveSizePolicy 的设置, 就不需要手动地调节新生代 (-Xmn) 大小, Eden 和 Survivor 区的比例 (-XX:SurvivorRatio), 晋升老年代对象年龄(-XX:PretenureSizeThreshold) 等细节参数, 而是根据当前系统运行情况来确定这些参数, 从而提高程序地吞吐量和缩短停顿时间, 这一过程称之为 GC 自适应的调节策略(GC Ergonomics).
另外值得注意的一点是, Parallel Scavenge 无法已 CMS 配合使用, 如果新生代选择了 Parallel Scavenge 收集器, 那么老年代的收集器只能选用 Serial Old 或者 Parallel Old 来配置使用.
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 也是单线程工作的, 使用的是 "标记 - 整理" 算法.
该收集器主要用于 Client 模式下的虚拟机使用, 如果在 Server 模式下可以与 Parallel Scavenge 收集器配合使用; 作为 CMS 收集器的后备预案, 在并发收集发生 Concurrent Mode Failure 时使用. 运行流程如下:
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 的老年代版本, 也是一个并行收集器, 使用 "标记 - 整理" 算法. 该收集器在 jdk1.6 后对外提供使用, Parallel Scavenge 和 Parallel Old 配合使用的话, 更加适合应用于高吞吐量和 CPU 敏感资源的场合. 下面是这两个收集器配合使用的运行流程:
CMS 收集器
CMS(Concurrent Mark Sweep)是一个并发收集器, 使用了 "标记 - 清除" 算法来实现的. 该收集器最求的更短的停顿时间, 从而提升用户体验, 因此也非常符合使用在网站, B/S 系统的服务端的应用.
CMS 收集器的工作流程大概可以分为以下 4 个步骤:
初始标记: 这个阶段仅仅标记能够和 gc roots 直接关联的对象, 速度很快, 但是需要 "stop the world".
并发标记: 这个阶段开始进行 gc roots tracing 标记, 与用户线程一起执行的, 消耗时间很多.
重新标记: 这个阶段是要是修正在并发标记期间由于用户线程也在运行而产生标记变动的那部分对象的标记, 比较耗费的时间比初始标记阶段要长, 但是远比并发标记阶段要短, 这个过程也是需要 "stop the world" 的.
并发清除: 对无用对象进行回收操作. 这个过程与用户线程并行执行.
由于标记和清除阶段可以和用户线程一起工作, 因此几乎可以把 CMS 收集器的工作是并发的:
CMS 是一款优秀的收集器, 它的主要优点是低停顿, 并发收集, 因此也被成为并发低停顿收集器(Concurrent Low Pause Collector).
当然, CMS 收集器也有一定的缺点, 主要包括一下几点:
CMS 收集器使用 "标记 - 清除" 算法实现, 因此不可避免地有内存碎片地问题. 当内存碎片过多时, 在分配大对象地过程中即使有足够的空间, 但是找不到足够地连续的空间来放该对象, 那么就有可能触发一次 full gc.
无法处理浮动垃圾(Floating Garbage) 可能出现 "Concurrent Mode Failure" 失败而导致另一次 Full GC 的产生. 这是因为在标记的过程中用户线程也在运行着, 那么在这一过程中出现的垃圾无法立即回收, 而是等下一次 gc 才能清理, 我这部分的垃圾就叫做 "浮动垃圾". 也是由于在垃圾收集阶段用户线程还需要运行, 那也就还需要预留有足够的内存空间给用户线程使用, 因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集, 需要预留一部分空间提供并发收集时的程序运作使用.
对 CPU 资源非常敏感. 其实, 只要是面对并发的情况下都会有这个问题, 在并发阶段虽然不会中断用户线程, 但是因为占用了部分用户的资源而导致程序变慢, 总吞吐量降低. CMS 搜集器默认的线程数 = (CPU 核数 + 3) / 4, 当 CPU 数量大于 4 时, 垃圾回收线程数不少于 25%, 随着线程数的增加而下降, 当 CPU 数量小于 4 时对线程的执行效率有显著的影响.
运行示意图如下:
G1 收集器
G1(Garbage-First)是一款面向服务端应用的垃圾收集器, JDK 7 Update4 后开始进入商用. HotSpot 开发团队赋予它的使命是未来可以替换掉 JDK 1.5 中发布的 CMS 收集器. 之前提供的收集器都是仅作用于新生代或者是老年代, 但是 G1 收集器可以作用于新生代和老年代, 因为使用 G1 收集器是 java heap 的内存结构有很大的不同, 它将整个 Java 堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但是他们已经没有了物理上的隔阂了, 它们都是 region 的一部分的集合.
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一, 与其他收集器相比, G1 收集器具有以下特征:
并行与并发: G1 能充分利用多 CPU, 多核环境下的硬件优势, 使用多个 CPU 来缩短 Stop-The-World 停顿时间, 部分其他收集器原本需要停顿 java 线程执行的 GC 动作, G1 收集器仍然可以通过并发的方式让 java 程序继续执行.
分代收集: 与其他收集器一样, 分代概念在 G1 中仍然得以保留. 虽然 G1 可以不需要其他收集器配合能够独立管理整个堆, 但它能够采用不用的方式去处理新创的对象和已经存活了一段世纪那, 熬过多次 GC 的旧对象以获得更好的收集效果.
空间整合: 与 CMS 的 "标记 - 清除" 算法不同, G1 整体来看采用了 "标记 - 整理" 算法实现的收集器, 从局部 (两个 Region 之间) 上来看是基于 "复制" 算法实现的. 无论使用哪一种方法, 都意味着 G1 运作期间不会产生内存空间碎片的问题, 收集后能提供规整的可用空间. 这种特性有利于程序长时间运行, 分配大对象是不会因为无法得到连续内存空间而提前处罚一次 GC.
可预测的停顿: 这是 G1 相对于 CMS 的另一大优势, 降低停顿时间是 G1 和 CMS 共同的关注点, 但 G1 除了最求低停顿外, 还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为 M 毫秒的时间片段内, 消耗在垃圾收集上的时间不得超过 N 毫秒, 这几乎是 java(RTSJ)的垃圾收集器的特征了.
G1 收集器之所以能够建立可预测的停顿时间模型, 因为他能够有计划地避免在整个 Java 堆中进行全区域的垃圾收集. G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的经验值), 在后台维护一个优先表, 每次根据允许的收集时间, 优先回收价值最大的 Region. 这种使用 Region 划分内存空间以及有优先级的区域回收方式, 保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率.
在 G1 收集器中, Region 之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用, 虚拟机都是使用 Remembered Set 来避免全堆扫描的. G1 中每个 Region 都有一个与之对应的 Remembered Set, 虚拟机发现程序在对 Reference 类型的数据进行写操作时, 会产生一个 Write Barrier 暂时中断写操作, 检查 Reference 引用的对象是否处于不同的 Region 中, 如果是, 便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 中的 Remembered Set 之中. 当进行内存回收时, 在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描, 也不会有遗漏.
如果不计算维护 Remembered Set 的操作, G1 收集器的运作大致分为以下几个步骤:
初始标记 (Initial Marking): 这阶段仅仅只是标记 GC Roots 能直接关联到的对象并修改 TAMS(Next Top at Mark Start) 的值, 让下一阶段用户程序并发运行时, 能在正确的可用的 Region 中创建新对象, 这阶段需要停顿线程, 但是耗时很短.
并发标记(Concurrent Marking): 从 GC Roots 开始对堆的对象进行可达性分析, 找出存活的对象, 这阶段耗时长, 但是可以与用户程序并发执行.
最终标记(Final Marking): 为了修正在并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分标记记录, 虚拟机将这段时间对象变化记录记录在线程 Remembered Set Logs 里面.
筛选回收(Live Data Counting and Evacuation): 首先对各个 Region 的回收价值和成本进行排序, 根据用户所期望的 GC 停顿时间来制定回收计划, 这一阶段是可以与用户程序一起并发执行的, 但是因为只回收部分 Region, 时间是用户可控的, 而且停顿用户线程将大幅度提高收集效率.
执行流程如下图:
总结
收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
Serial Old | 串行 | 老年代 | 标记 - 整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记 - 整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记 - 清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
G1 | 并发 | both | 标记 - 整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
参考资料: 《深入理解 Java 虚拟机 - JVM 高级特性与最佳实践》 - 周志明
来源: https://www.cnblogs.com/rainple/p/10829144.html