堆内存常见的分配策略、 经典的垃圾收集器、CMS与G1收集器及二者的比较

馋奶兔 提交于 2020-04-29 10:51:35

堆内存常见的分配策略

针对的是Serial 加 Serial Old 客户端默认收集器组合下的内存分配和回收策略

 

 

 经典的垃圾收集器

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的垃圾收集器。从名字可以看出,CMS 是基于标记-清除算法的。它的运作过程主要分为四个步骤:

  1. 初始标记(CMS initial mark):STW,标记GC Roots能直接关联到的对象,速度很快。单线程
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长,不需要停顿用户线程
  3. 重新标记(CMS remark):STW,修正并发标记期间,因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录(增量更新),时间稍长于初始标记,但远低于并发标记
  4. 并发清除(CMS concurrent sweep):清除已死亡对象,因为不需要移动对象,所以与用户线程是并发的关系  

CMS 的缺点:

  • CMS收集器对处理器资源敏感。在并发阶段,虽然不会导致用户停顿(STW),但会占用一部分线程(计算机资源)导致应用程序变慢,吞吐量降低。CMS默认开启回收线程数是 (处理器核心数量 + 3)/4。当处理器核心数量大于4个时,垃圾回收线程只占不超过 25%的处理器资源,且随着处理器核心数量增加而降低。但处理器核心数量不足4个时,会严重影响用户程序。为了缓解这个问题,虚拟机提供了增量式并发收集器的CMS变种,在并发标记和并发清理过程中,垃圾收集线程和用户线程交替运行,但效果一般,已标记为废弃。
  • 无法处理“浮动垃圾”,有可能出现 Concurrent Mode Failure失败,导致进一步 STW 的Full GC在并发标记和并发清理过程中,用户线程还在运行,自然还会伴随新的垃圾对象的产生,但该部分的垃圾对象出现在标记过程结束以后,CMS无法在当次收集过程中处理他们(只能下次垃圾收集时处理),因此称为浮动垃圾。因此,CMS必须预留足够的内存空间提供给用户线程使用,JDK6,默认启动阈值为 92%(该值需要根据生产环境变化,太高,有可能频繁并发失败):要是CMS运行期间预留的内存无法满足程序分配新对象,触发并发失败(Concurrent Mode Failure),这时候JVM启动预备方案:冻结用户线程,临时启动 Serial Old 来进行老年代的手机,这样停顿时间会很长
  • 大量空间碎片。空间碎片过多会导致大对象分配失败(老年代还有很多空间,但没有连续的大空间),而不得不提前触发 Full GC。CMS提供了参数 -XX:+UseCMS-CompactAtFullCollection 开关参数(默认开启,JDK9之后废弃),用于在CMS不得不Full GC时开启碎片的合并整理过程。但整理涉及到移动对象,STW,停顿时间会变长。

 Garbage First (G1收集器)

目标是在延迟可控的范围内获取尽可能高的吞吐量

G1是面向堆内任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式。 

G1不再坚持分代划分,而是把连续的Java 堆划分为多个大小相等的独立区域(Region),每个 Region 根据需要版本新生代的Eden、Survivor或老年代。收集器能够对扮演不同角色的Region采用不同的策略去处理。这样无论是新对象还是已经存活了一段时间的对象、熬过多次收集的就对象都能获得很好的收集效果。

Region 中还有一类特殊的 Humonggous 区域,专门用来存放大对象此处大对象,指的是内存大小超过一个 Region 容量一半的对象。每个Region的大小可以通过 -XX:G1HeapRegionSize 设定,取值范围为 1~32MB,且为2的N次幂。如果对象大小超过Region大小,将会被放到N个连续的 Humongous Region,G1的大多数行为将Humongous 作为老年代的一部分处理。

虽然 G1 仍然保留了新生代和老年代的概念,但新生代和老年代不再是固定的,他们都是一系列区域(不需要连续)的动态集合。G1 之所以能够建立可预测的停顿时间模型,是因为他将Region 作为可回收的最小单元。更具体的操作是:让G1 去跟踪各个Region 里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需要的经验值,然后在后台维护一个优先级列表,每次根据用户设定的允许的收集停顿时间(-XX:MaxGCPauseMills,默认200ms)优先处理回收价值最大的Region。这也是 “Garbage First“ 的由来。该方式保证了G1在有限的事件内获取尽可能高的收集效率。

 

G1收集器大致可以分为以下四个步骤:

  1. 初始标记(initial marking):标记GC Roots 直接关联的对象,并修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确的在可用的 Region中分配新对象。STW,耗时很短
  2. 并发标记(concurrent marking):从 GC Roots 开始对堆中对象进行可达性分析,递归扫面整个堆里的对象图。耗时长,但可与用户线程并发执行。对象图扫描完成后,还要重新处理 SATB记录下的在并发时有引用变动的对象
  3. 最终标记(final marking):暂停用户线程(很短),用于处理并发阶段结束后,遗留下来的最后少量的SATB记录
  4. 筛选回收(Live data counting and evacuation 疏散、撤退):负责更新 Region 的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来指定计划,可自由选择任意多个 Region 进行回收。把决定回收的那部分的Region的存活对象复制到空的Region中,然后清理掉整个旧Region的空间。因为涉及对象的移动,所以需要STW,暂停用户线程,由多条收集线程并行完成。

只有并发标记可以与用户线程并发进行。

CMS 与 G1 比较

G1 的优点

  1. 收集算法不同,CMS 是标记—清除,G1从整体来看是基于”标记—整理“的,但从局部看(两个Region之间),又是基于”复制“的。这意味着,G1不会产生内存碎片,垃圾回收完成后内存规整,在程序为大对象分配内存时不容易因为无法找到连续内存空间而提前触发Full GC 
  2. 可以指定最大停顿时间
  3. 分 Region 的内存布局
  4. 按受益动态确定回收集

G1 的缺点:

  1. 就内存占用而言,虽然 G1 和 CMS 都适用卡表来处理跨代指针,但G1 的实现更复杂,而且每个Region都需要维护一份卡表。而CMS 的卡表只有唯一一份,只需要处理老年代到新生代的引用。这导致G1的记忆集(和其他内存消耗)会占整个堆容量的 20%甚至更多。
  2. 就执行负载而言,两个收集器各自的实现细节导致了用户程序运行时的负载会有不同。譬如,两者都用到写屏障,(待续,还没看懂)

CMS 更适合在小内存应用上,而G1在大内存应用上能发挥更多优势。两者的内存临界点为 6 GB 到 8GB。

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!