GC相关知识整理
1.8 默认GC算法
默认使用的是 Parallel Scavenge (新生代) 和 Parallel Old (老年代)
为什么要分代
根据经验分析,绝大多数对象存货时间较短,将存活时间短的对象与存活时间长的对象分开存储,让GC更高效。
目前的GC
垃圾收集器
特点 | 采用算法 | 关注 | 其他 | |
---|---|---|---|---|
Serial | 串行的 | 复制算法 | ||
Serical Old | 串行的 | 标记整理算法 | ||
ParNew | 新生代并行 老年代串行 | 新生代复制算法 老年代标记整理算法 | ||
Parallel Scavenge | 并行的 | 复制算法 | 提升吞吐量 | 其他GC参数 -XX:MaxGCPauseMills 最大停顿时间,单位毫秒 GC尽力保证回收时间不超过设定值 -XX:GCTimeRatio 0-100的取值范围 垃圾收集时间占总时间的比 默认99,即最大允许1%时间做GC |
Parallel Old | 并行的 | 标记整理算法 | ||
CMS | 并行的 | 标记-清除算法 | 减少停顿时间 尽可能降低停顿 | 会影响系统整体吞吐量和性能 |
G1 | 并行的 | 新生代复制算法,老年代标记整理算法 | 空间整合:不会产生内存碎片 可预测的停顿 | G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由) |
serial
单线程的
mirro GC和major GC都需要暂停应用(即stop-the-world)
Parallel GC
在parallel GC 中,minor 和 full GC 都是并行的,可使用多核并行回收,可指定使用多少线程,能很好改善应用吞吐量,一般的机器配置中,如果不指定所使用的GC collector,一般默认为 Parallel GC,它符合大多数应用场景。如下图:
CMS
该回收器的出现主要应对低响应延时的应用场景,即 stop-the-world 一旦发生,应用将停止响应直到它工作完成,这情况在很多WEB、电信或银行应用中尤为关键。虽然 major GC很少发生,但是一旦发生将耗时较长,特别是在Heap很大的时候(也就是我们为什么不能让Heap分配很大的原因,适当即可,可有效分散GC暂停的时间使得不会感到明显卡顿)。
CMS GC 旨在减少应用的响应时间(但会牺牲一些吞吐量),由于延长了GC的工作周期,所以有更大的heap区要求(在marking pause阶段仍然允许分配内存),它同时具有更复杂的算法,CMS GC在管理 young generation方面与 Serial/Parallel GC一样(通过参数指定,稍候的文章中会有详解),在管理 old generation 采用2个周期来回收以减少应用暂停时间,
说明:1. 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降。2. 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。3. 由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长,虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的GC。
https://www.jianshu.com/p/86e358afdf17
回收过程:
-
初始标记 标记老年区之外的所有可以直接抵达的对象(STW)
标记老年代中所有的GC Roots对象,如下图节点1;
标记年轻代中活着的对象引用到的老年代的对象(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3; -
并发标记 标记所有可抵达的存活对象,不需要STW
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
由于这个阶段是和用户线程并发的,可能会导致concurrent mode failure。 -
预处理 为了减轻Remark pause的工作
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty; -
可被终止的预清理
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻带的引用,是的下个阶段的重新标记阶段,扫描年轻带指向老年代的引用的时间减少; -
重新标记 STW
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻带的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled。 -
并发清除
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。 -
并发重置状态等待下次CMS的触发
CMS是基于标记-清除算法的,CMS只会删除无用对象,不会对内存做压缩,会造成内存碎片,这时候我们需要用到这个参数:
-XX:CMSFullGCsBeforeCompaction=n
意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩。
早提升的原因
Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc;
对象太大,Survivor和Eden没有足够大的空间来存放这些大对象。
提升失败原因
当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。为什么是没有足够的连续空间而不是空闲空间呢?老年代容纳不下提升的对象有两种情况:
老年代空闲空间不够用了;
老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象。
解决方法
如果是因为内存碎片导致的大对象提升失败,cms需要进行空间整理压缩;
如果是因为提升过快导致的,说明Survivor 空闲空间不足,那么可以尝试调大 Survivor;
如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低。
CMS收集器只收集老年代,其以吞吐量为代价换取收集速度。
CMS收集过程分为:初始标记、并发标记、预清理阶段、可终止预清理、重新标记和并发清理阶段。其中初始标记和重新标记是STW的。CMS大部分时间都花费在重新标记阶段,可以让虚拟机先进行一次Young GC,减少停顿时间。CMS无法解决"浮动垃圾"问题。
由于CMS的收集线程和用户线程并发,可能在收集过程中出现"concurrent mode failure",解决方法是让CMS尽早GC。在一定次数的Full GC之后让CMS对内存做一次压缩,减少内存碎片,防止年轻代对象晋升到老年代时因为内存碎片问题导致晋升失败。