在以前的几篇博客中,咱们大体介绍了,常见的 垃圾回收算法 及 JVM
中常见的分类回收算法。这些都是从算法和规范上分析 Java
中的垃圾回收,属于方法论。在 JVM
中,垃圾回收的具体实现是由 垃圾回收器(Garbage Collector
)负责。算法
在了解 垃圾回收器 以前,首先得了解一下垃圾回收器的几个名词。编程
CPU
用于运行用户代码的时间与 CPU
总消耗时间的比值。好比说虚拟机总运行了 100
分钟,用户代码 时间 99
分钟,垃圾回收 时间 1
分钟,那么吞吐量就是 99%
。后端
吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾回收时间)缓存
停顿时间 指垃圾回收器正在运行时,应用程序 的 暂停时间。对于 独占回收器 而言,停顿时间可能会比较长。使用 并发回收器 时,因为垃圾回收器和应用程序 交替运行,程序的 停顿时间 会变短,可是,因为其 效率 极可能不如独占垃圾回收器,故系统的 吞吐量 可能会较低。多线程
指发生在 新生代 的垃圾回收动做,由于 Java
对象大多都具有 朝生夕死 的特性,因此 Minor GC
一般 很是频繁,通常回收速度也比较快。架构
指发生在 老年代 的垃圾回收动做,出现了 Major GC
,常常会伴随至少一次的 Minor GC
(发生这种状况,那么 整个堆 都 GC
一遍,一般称为 Full GC
)。Major GC
的速度通常会比 Minor GC
慢 10
倍以上。并发
单线程 进行垃圾回收工做,但此时 用户线程 仍然处于 等待状态。框架
这里的并发指 用户线程 与 垃圾回收线程 交替执行。异步
这里的并行指 用户线程 和多条 垃圾回收线程 分别在不一样 CPU
上同时工做。分布式
根搜索算法 是从 离散数学 中的图论引入的,程序把全部引用关系看做一张图,从一个节点 GC ROOT
开始,寻找对应的 引用节点,找到这个节点后,继续寻找 这个节点 的 引用节点。当全部的引用节点寻找完毕后,剩余的节点 则被认为是 没有被引用到 的节点,即 无用 的节点。
上图 红色 为无用的节点,能够被 回收。目前 Java
中能够做为 GC ROOT
的对象有:
虚拟机栈 中引用的对象(本地变量表);
方法区 中 静态变量 引用的对象;
方法区 中 常量 引用的对象;
本地方法栈 中引用的对象(Native
对象)。
基本全部
GC
算法都引用 根搜索算法 这种概念。
标记-清除算法 从 根集合 进行扫描,对 存活的对象 进行 标记。标记完毕后,再扫描整个空间中 未被标记 的对象进行 直接回收,以下图所示:
标记-清除算法 不须要进行 对象的移动,而且仅对 不存活 的对象进行处理,在 存活 的对象 比较多 的状况下 极为高效。但因为 标记-清除算法 直接回收不存活的对象,并无对还存活的对象进行 整理,所以会致使 内存碎片。
复制算法 将内存划分为 两个区间,使用此算法时,全部 动态分配 的对象都只能分配在 其中一个 区间(活动区间),而 另一个 区间(空间区间)则是 空闲 的。
复制算法 一样从 根集合 扫描,将 存活 的对象 复制 到 空闲区间。当扫描完毕活动区间后,会的将 活动区间 一次性所有 回收。此时本来的 空闲区间 变成了 活动区间。下次 GC
时候又会重复刚才的操做,以此循环。
复制算法 在存活对象 比较少 的时候,极为高效,可是带来的成本是 牺牲一半的内存空间 用于进行 对象的移动。因此 复制算法 的使用场景,必须是对象的 存活率很是低 才行。最重要的是,咱们须要克服 50%
的 内存浪费。
标记-整理算法 采用 标记-清除算法 同样的方式进行对象的 标记,但在回收 不存活的对象 占用的空间后,会将全部 存活的对象 往 左端空闲空间 移动,并更新对应的指针。
标记-整理 是在 标记-清除 之上,又进行了 对象的移动排序整理,所以 成本更高,但却解决了 内存碎片 的问题。
JVM
为了 优化内存 的回收,使用了 分代回收 的方式。对于 新生代内存 的回收(Minor GC
)主要采用 复制算法。而对于 老年代内存 的回收(Major GC
),大多采用 标记-整理算法。
在 JVM
中,具体实现有 Serial
、ParNew
、Parallel Scavenge
、CMS
、Serial Old(MSC)
、Parallel Old
、G1
等。在下图中,你能够看到 不一样垃圾回收器 适合于 不一样的内存区域,若是两个垃圾回收器之间 存在连线,那么表示二者能够 配合使用。
若是当 垃圾回收器 进行垃圾清理时,必须 暂停 其余全部的 工做线程,直到它彻底收集结束。咱们称这种须要暂停工做线程才能进行清理的策略为 Stop-the-World
。以上回收器中, Serial
、ParNew
、Parallel Scavenge
、Serial Old
、Parallel Old
均采用的是 Stop-the-World
的策略。
图中有 7
种不一样的 垃圾回收器,它们分别用于不一样分代的垃圾回收。
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
两个 垃圾回收器 之间有连线表示它们能够 搭配使用,可选的搭配方案以下:
新生代 | 老年代 |
---|---|
Serial | Serial Old |
Serial | CMS |
ParNew | Serial Old |
ParNew | CMS |
Parallel Scavenge | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
Serial
回收器是最基本的 新生代 垃圾回收器,是 单线程 的垃圾回收器。因为垃圾清理时,Serial
回收器 不存在 线程间的切换,所以,特别是在单 CPU
的环境下,它的 垃圾清除效率 比较高。对于 Client
运行模式的程序,选择 Serial
回收器是一个不错的选择。
Serial
新生代回收器 采用的是 复制算法。
Serial Old
回收器是 Serial
回收器的 老生代版本,属于 单线程回收器,它使用 标记-整理 算法。对于 Server
模式下的虚拟机,在 JDK1.5
及其之前,它常与 Parallel Scavenge
回收器配合使用,达到较好的 吞吐量,另外它也是 CMS
回收器在 Concurrent Mode Failure
时的 后备方案。
Serial
回收器和 Serial Old
回收器的执行效果以下:
Serial Old
老年代回收器 采用的是 标记 - 整理算法。
ParNew
回收器是在 Serial
回收器的基础上演化而来的,属于 Serial
回收器的 多线程版本,一样运行在 新生代区域。在实现上,二者共用不少代码。在不一样运行环境下,根据 CPU
核数,开启 不一样的线程数,从而达到 最优 的垃圾回收效果。对于那些 Server
模式的应用程序,若是考虑采用 CMS
做为 老生代回收器 时,ParNew
回收器是一个不错的选择。
ParNew
新生代回收器 采用的是 复制算法。
和 ParNew
回收同样,Parallel Scavenge
回收器也是运行在 新生代区域,属于 多线程的回收器。但不一样的是,ParNew
回收器是经过控制 垃圾回收 的 线程数 来进行参数调整,而 Parallel Scavenge
回收器更关心的是 程序运行的吞吐量。即一段时间内,用户代码运行时间占 总运行时间 的百分比。
Parallel Scavenge
新生代回收器 采用的是 复制算法。
Parallel Old
回收器是 Parallel Scavenge
回收器的 老生代版本,属于 多线程回收器,采用 标记-整理算法。Parallel Old
回收器和 Parallel Scavenge
回收器一样考虑了 吞吐量优先 这一指标,很是适合那些 注重吞吐量 和 CPU
资源敏感 的场合。
Parallel Old
老年代回收器 采用的是 标记 - 整理算法。
CMS(Concurrent Mark Sweep)
回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法。
相比以前的回收器,CMS
回收器的运做过程比较复杂,分为四步:
初始标记 仅仅是标记 GC Roots
内 直接关联 的对象。这个阶段 速度很快,须要 Stop the World
。
并发标记 进行的是 GC Tracing
,从 GC Roots
开始对堆进行 可达性分析,找出 存活对象。
从新标记 阶段为了 修正 并发期间因为 用户进行运做 致使的 标记变更 的那一部分对象的 标记记录。这个阶段的 停顿时间 通常会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也须要 Stop The World
。
并发清除 阶段会清除垃圾对象。
初始标记(
CMS initial mark
)和 从新标记(CMS remark
)会致使 用户线程 卡顿,Stop the World
现象发生。
在整个过程当中,CMS
回收器的 内存回收 基本上和 用户线程 并发执行,以下所示:
因为 CMS
回收器 并发收集、停顿低,所以有些地方成为 并发低停顿回收器(Concurrent Low Pause Sweep Collector
)。
CMS
回收器的缺点:
CMS
回收器过度依赖于 多线程环境,默认状况下,开启的 线程数 为(CPU 的数量 + 3)/ 4
,当 CPU
数量少于 4
个时,CMS
对 用户查询 的影响将会很大,由于他们要分出一半的运算能力去 执行回收器线程;
因为 CMS
回收器 清除已标记的垃圾 (处于最后一个阶段)时,用户线程 还在运行,所以会有新的垃圾产生。可是这部分垃圾 未被标记,在下一次 GC
才能清除,所以被成为 浮动垃圾。
因为 内存回收 和 用户线程 是同时进行的,内存在被 回收 的同时,也在被 分配。当 老生代中的内存使用超过必定的比例时,系统将会进行 垃圾回收;当 剩余内存 不能知足程序运行要求时,系统将会出现 Concurrent Mode Failure
,临时采用 Serial Old
算法进行 清除,此时的 性能 将会下降。
CMS
回收器采用的 标记清除算法,自己存在垃圾收集结束后残余 大量空间碎片 的缺点。CMS
配合适当的 内存整理策略,在必定程度上能够解决这个问题。
G1
是 JDK 1.7
中正式投入使用的用于取代 CMS
的 压缩回收器。它虽然没有在物理上隔断 新生代 与 老生代,可是仍然属于 分代垃圾回收器。G1
仍然会区分 年轻代 与 老年代,年轻代依然分有 Eden
区与 Survivor
区。
G1
首先将 堆 分为 大小相等 的 Region
,避免 全区域 的垃圾回收。而后追踪每一个 Region
垃圾 堆积的价值大小,在后台维护一个 优先列表,根据容许的回收时间优先回收价值最大的 Region
。同时 G1
采用 Remembered Set
来存放 Region
之间的 对象引用 ,其余回收器中的 新生代 与 老年代 之间的对象引用,从而避免 全堆扫描。G1
的分区示例以下图所示:
这种使用 Region
划分 内存空间 以及有 优先级 的区域回收方式,保证 G1
回收器在有限的时间内能够得到尽量 高的回收效率。
G1
和 CMS
运做过程有不少类似之处,整个过程也分为 4
个步骤:
初始标记 仅仅是标记 GC Roots
内 直接关联 的对象。这个阶段 速度很快,须要 Stop the World
。
并发标记 进行的是 GC Tracing
,从 GC Roots
开始对堆进行 可达性分析,找出 存活对象。
从新标记 阶段为了 修正 并发期间因为 用户进行运做 致使的 标记变更 的那一部分对象的 标记记录。这个阶段的 停顿时间 通常会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也须要 Stop The World
。
首先对各个 Region
的 回收价值 和 成本 进行排序,根据用户所指望的 GC
停顿时间 来制定回收计划。这个阶段能够与用户程序一块儿 并发执行,可是由于只回收一部分 Region
,时间是用户可控制的,并且停顿 用户线程 将大幅提升回收效率。
与其它
GC
回收相比,G1
具有以下4
个特色:
使用多个 CPU
来缩短 Stop-the-World
的 停顿时间,部分其余回收器须要停顿 Java
线程执行的 GC
动做,G1
回收器仍然能够经过 并发的方式 让 Java
程序继续执行。
与其余回收器同样,分代概念 在 G1
中依然得以保留。虽然 G1
能够不须要 其余回收器配合 就能独立管理 整个GC堆,但它可以采用 不一样的策略 去处理 新建立的对象 和 已经存活 一段时间、熬过屡次 GC
的旧对象,以获取更好的回收效果。新生代 和 老年代 再也不是 物理隔离,是多个 大小相等 的独立 Region
。
与 CMS
的 标记—清理 算法不一样,G1
从 总体 来看是基于 标记—整理 算法实现的回收器。从 局部(两个 Region
之间)上来看是基于 复制算法 实现的。
但不管如何,这 两种算法 都意味着 G1
运做期间 不会产生内存空间碎片,回收后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象 时不会由于没法找到 连续内存空间 而提早触发 下一次 GC
。
这是 G1
相对于 CMS
的另外一大优点,下降停顿时间 是 G1
和 CMS
共同的关注点。G1
除了追求 低停顿 外,还能创建 可预测 的 停顿时间模型,能让使用者明确指定在一个 长度 为 M
毫秒的 时间片断 内,消耗在 垃圾回收 上的时间不得超过 N
毫秒。(后台维护的 优先列表,优先回收 价值大 的 Region
)。
周志明,深刻理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
欢迎关注技术公众号:零壹技术栈
本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。