https://www.jianshu.com/p/bdd6f03923d1
CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc
和Mixed gc
,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
2. 如何处理跨代引用
在垃圾回收的时候都是从Root开始搜索,这会先经过年轻代再到老年代,对于年轻代引用老年代的这种跨代不需要单独处理。但是老年代引用年轻代的会影响young gc
,这种跨代需要处理。
为了避免在回收年轻代的时候扫描整个老年代,需要记录老年代对年轻代的引用,young gc
的时候只要扫描这个记录。CMS和G1都用到了Card Table
,但是用法不太一样。JVM将内存分成一个个固定大小的card
,然后有一个专门的数据结构(即这里的Card Table
)维护每个Card
的状态,一个字节对应一个Card
,有点像内存page的概念,只是page是硬件上的,Card Table
是软件上的。当一个Card
上的对象的引用发生变化的时候,就将这个Card
对应的Card Table
上的状态置为dirty,young gc
的时候扫描状态是dirty
的Card
即可。这是基本的用法,CMS基本上就是这么使用。
G1在Card Table
的基础上引入的remembered set
(下面简称RSet
)。每个region都会维护一个RSet
,记录着引用到本region中的对象的其他region的Card
。比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录B所在的Card
的地址。这样的好处是可以对region进行单独回收,这要求RSet不只是维护老年代到年轻代的引用,也要维护这老年代到老年代的引用,对于跨代引用的每次只要扫描这个region的RSet上的Card
即可。
上面说过年轻代到老年代的引用不需要单独处理,这带来了很大的性能上的提升,因为年轻代的对象引用变化很大,如果都需要记录下来成本会很高。同时也说明只需要在老年代维护Card Table
。
3. 如何处理并发过程的对象变化
CMS和G1都有并发处理过程,这个过程应用程序跟着gc线程一起运行,会产生新对象,也会有旧的对象死去,对象之间的引用关系也会发生变化。这部分数据可以暂时不处理,留到下一次再处理吗?如果可以这样的话问题就会变得很简单,但是答案是不行。考虑下图的场景(图中每一行表示一个内存状态,每一列表示一个Card
,这里有4个):第一步a是并发标记中途的一个状态,标记了a b c e四个对象,0 1两个Card
已经标记好;第二步b并发标记的同时引用发生变化,g不再指向d,而b不再指向c,变成指向d,这个时候处理Card 2
,会标记到g,然后就标记结束了,导致d对象丢失。
CMS初始标记的时候会标记所有从root直接可达的对象,并发标记的时候再从这些对象进一步搜索其他可达对象,最终构成一个存活的对象图。并发标记过程中引用发生变化的也是通过Card Table
来记录。但是young gc
的时候如果一个dirty card
没有包含到年轻代的引用,这个card会重新标记为clean,这有可能将并发标记过程产生的dirty card
错误清除,因此CMS引入了另一个数据结构mod union table
,这里一个bit对应一个Card
,young gc
在将Card Table
设置为clean的时候会将对应的mod union table
置为dirty。最终标记的时候会将Card Table
或者mod union table
是dirty的Card
也作为root去扫描,从而解决并发标记过程产生的引用变化。CMS还需要处理并发过程从年轻代晋升到老年代的对象,处理方式是将这部分对象也作为root去扫描。
G1使用一个称为snapshot at the beginning
(下面简称SATB
)的算法,在初始标记的时候得到一个从root直接可达的snapshot
,之后从这个snapshot
不可达的对象都是可以回收的垃圾,并发过程产生的对象都默认是活的对象,留到下一次再处理。对于引用关系发生变化的,将这个对象对应的Card
放到一个SATB
队列里,在最终标记的时候进行处理(如果超过一定的阈值并发标记的时候也会处理一部分),处理的过程就是以队列中的Card
作为root进行扫描
4. Write Barrier
Write Barrier
可以理解为在写的时候插入一条特定的操作。
在CMS中老年代引用年轻代的时候就是通过触发一个Write Barrier
来更新Card Table
的标志位。这是一个同步操作,在更新引用的时候顺带执行,只需要两个指令,引入的消耗不大。
G1比较复杂,在两个地方用到了Write Barrier
,分别是更新RSet的rememberd set Write Barrier
和记录引用变化的Concurrent Marking Write Barrier
,前者发生在引用更新之后,称为Post Write Barrier
,后者发生在引用变化之前,称为Pre Write Barrier
。G1为了提高性能,这两个Write Barrier
都是先放到队列中,再异步进行处理。具体可以参考Garbage-First Garbage Collection 论文笔记
5. Full GC
导致CMS Full GC的可能原因主要有两个:Promotion Failure
和Concurrent Mode Failure
,前者是在年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的;后者是在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。CMS的Full GC是一个多线程STW的Mark-Compact过程,,需要尽量避免或者降低频率。
G1的初衷就是要避免Full GC的出现,Full GC会会对所有region做Evacuation-Compact,而且是单线程的STW,非常耗时间。导致G1 Full GC的原因可能有两个:1. Evacuation的时候没有足够的to-space来存放晋升的对象;2. 并发处理过程完成之前空间耗尽。这两个原因跟CMS类似。
CMS 调优
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
CMS收集器优点:并发收集、低停顿。
CMS收集器缺点:
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾(Floating Garbage)。
- CMS收集器是基于标记-清除算法,该算法的缺点都有。
CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供CMS版本。
G1具备如下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
- 分代收集
- 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:
- 实时数据占用了超过半数的堆空间;
- 对象分配率或“晋升”的速度变化明显;
- 期望消除耗时较长的GC或停顿(超过0.5——1秒)。
card table 标识,是否有指向新生代对象的引用;如果有就代表是脏卡 ; Minor GC就不能清理这个新生代的对象
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。
想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。
卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
混合式收集垃圾才会通过启发式算法,从老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中
写前栅栏 写后栅栏 ,维护Rset
丧失引用的对象,记录到SATB队列中,后面再批量更新RSet
写后栅栏:RSet也不会立即更新,先记录到更新日志,后面批量处理,使用refinement Threads更新
SATB创建对象图,每个线程都会独占一个SATB缓冲区;并发标记:更新RSet
扫描日志缓冲区记录的卡片来更新Rest,异步更新,而不是之前立刻就更新,如果更新都跟不上缓冲区写入速度,就Mutator线程挂起,协助更新Rset,造成应用线程处理失败。
重新标记处理SATB日志缓冲区和所有更新,更新Rset
G1 垃圾收集器架构和如何做到可预测的停顿(阿里)
+++++++++++++++++++++++++++++++++++++++++++++++
CMS和G1的三色标记算法
CMS处理方式和G1是不同的
按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。
白色垃圾会对象,被黑色对象引用了,这时候就不能回收这个白色对象;
写屏障,保证B->D的引用,还可以遍历;这样保证D对象本轮不能回收,产生了浮动垃圾,但是不会被误回收
CMS是把B再标记成为灰色,collector重新再扫描
通过G1SATBCardTableModRefBS::enqueue(oop pre_val)
把原引用保存到satb mark queue中,和RSet的实现类似,每个应用线程都自带一个satb mark queue.
在下一次的并发标记阶段,会依次处理satb mark queue中的对象,确保这部分对象在本轮GC是存活的。
SATB 会造成白色对象,一部分失去灰色对象引用(标记为灰色),但是还没有被黑色对象引用。仍然无法清理,造成浮动垃圾。
CMS 处理方式低效在于:第一次gc线程标记后,应用线程又把白色垃圾对象B放到a的字段中,GC线程执行完标记后;就会把a对象变成黑色,此时仍然不能回收b;需要对堆空间重新进行扫描再次判断
这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
G1 垃圾收集器架构和如何做到可预测的停顿(阿里)
选择回收收益最大的内存块来回收;并且取决于用户配置的暂停时间,配置暂停时间少就少回收,暂停时间多就多回收一些
由于内存被分成了很多小块,又带来了另外好处,由于内存块比较小,进行内存压缩整理的代价都比较小,相比其它GC算法,可以有效的规避内存碎片的问题。
说了G1的这么多好处,也该说说G1的坏处了,如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。
G1的垃圾回收时间可预测,可以避免长时间垃圾收集或者压缩暂停
概述
G1垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器。G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。G1回收器和CMS比起来,有以下不同:
- G1垃圾回收器是compacting的,因此其回收得到的空间是连续的。这避免了CMS回收器因为不连续空间所造成的问题。如需要更大的堆空间,更多的floating garbage。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
- G1回收器的内存与CMS回收器要求的内存模型有极大的不同。G1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的;
G1还有一个及其重要的特性:软实时(soft real-time)。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
RSet介绍
在G1中,引入了RSet(Remember Set,记忆集)的概念,用来记录不同代际之间的引用关系,目的是为了加快垃圾回收的速度。
通常有两种方法记录引用关系,分别为point out和point in。比如a=b(a引用b),若采用point out结构,则在a的RSet中记录b的地址;若采用point in结构,则在b的RSet中记录a的地址。
G1的RSet采用的是point in结构,即谁引用了我。(Card Table采用的是point out结构)
为什么需要记录跨代的引用
JVM一般都会对内存进行分代处理,以提高内存分配和垃圾回收的效率。Minor GC只会回收年轻代,Major GC只会老年代,无论是哪一种GC都会面临跨代引用的情况,比如老年代对象引用新生代或者新生代对象引用老年代。
Minor GC在回收年轻代时,需要判断年轻代的对象是否存活,而年轻代的部分对象可能被老年代的对象引用,因此必须扫描老年代才不会发生误判年轻代的对象为垃圾;同理,在回收老年代时,也需要扫描年轻代。
那么无论是只回收新生代还是老年代,都需要扫描其他代的对象,相当于进行全堆扫描,效率很低。那么将代际之间的引用关系记录在一个单独的地方,只需要扫描这个地方即可,避免全堆扫描。
RSet带来的问题
RSet需要额外的内存空间来存储这些引用关系,一般是JVM最大的额外开销的1%-20%之间;
RSet中的对象可能已经死亡,那么这个时候引用的对象会被认为活跃对象,实际上它是浮动垃圾;
RSet是通过写屏障来完成的,即在内存分配的地方,插入一段代码来执行RSet的更新,如果对象的创建/修改/回收比较频繁,那么写RSet的性能开销还是比较大的。因此一般不会记录年轻代到老年代的引用。
G1的RSet设计
主要分析哪些引用的关系需要记录在RSet中;
分区内部的引用
无论是新生代还是老年代的分区内部的引用,都不需要记录引用关系。因为是针对一个分区进行的垃圾回收,要么这个分区被回收,要么不被回收。
新生代引用新生代
G1的三种回收算法(YGC/MIXED GC/FULL GC)都会全量处理新生代分区,所以新生代都会被遍历到。因此无需记录这种引用关系。
新生代引用老年代
无需记录。G1的YGC回收新生代,无需这个引用关系。混合GC时,G1会采用新生代分区作为根,那么在遍历新生代分区时就能找到老年代分区了,无需这个引用关系。对于FGC来说,所有分区都会被处理,也无需这个引用关系。
老年代引用新生代
需要记录。YGC在回收新生代时,如果新生代的对象被老年代引用,那么需要标记为存活对象。即此时的根对象有两种,一个是栈空间/全局变量的引用,一个是老年代到新生代的引用。
老年代引用老年代
需要记录。混合GC时,只会回收部分老年代,被回收的老年代需要正确的标记哪些对象存活。
RSet的更新
写屏障即在改变特定内存的值时,执行一些额外的动作。
G1的RSet的更新是通过写屏障完成的,在写变更时,通过插入一条额外的代码把引用关系放入到DCQ队列中,随后refine线程取出DCQ队列的引用关系,更新RSet。比如,每一次将一个老年代对象的引用修改为指向新生代对象时,都会被写屏障捕获,并且记录下来。
对于一个写屏障来时,过滤掉不必要的写操作是十分必要的,G1进行以下过滤:
不记录新生代到新生代的引用 或者 新生代到老年代的引用
过滤一个分区内部的引用
过滤空引用