Serial收集器
单线程收集器,收集时会暂停所有工作线程(Stop The World),虚拟机运行在Client模式时的默认新生代收集器。
- 最早的收集器,单线程进行GC
- New和Old Generation 都可以使用
- 在新生代,采用复制算法那;在老年代,采用Mark-Compact算法
- 因为是单线程GC, 没有多线程切换的额外开销,简单使用
-
Hotspot Client模式缺省的收集器
ParNew收集器
ParNew收集器就是Serial的多线程版本,除了多个收集器线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一模一样。
对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU环境中,ParNew收集器并不会比Serial收集器有更好的效果。
- Serial收集器在新生代的多线程版本
- 使用复制算法(针对新生代)
- 只有在多CPU的环境下,效率才会比Serial收集器高
- 可以通过-XX:ParallelGCThreads来控制GC的线程数。需要结合具体的CPU个数
- Server模式下新生代的缺省收集器
Parallel Scavenge收集器
Parallel Scavenge 收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换区总吞吐量最大化。
Serial Old收集器
Serial Old是单线程收集器,使用标记-整理算法,是老年代回收器。
Parallel Old收集器
老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM1.6提供,在此之前,新生代使用了PS收集器算法的话,老年代除Serial Old外别无选择,应为PS无法与CMS收集器配合工作。
- Parallel Scavenge在老年代实现
- 采用多线程,Mark-Compact算法
- 更注重吞吐量
- PS+PO = 高吞出量,但GC停顿可能不理想。
CMS收集器
CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,CMS收集器使用的标记-清除算法。
- 追求最短停顿时间,非常适合Web引用
- 只针对老年区,一般结合ParNew使用
- Concurrent, GC线程和用户线程并发工作(尽量并发)。
- 使用Mark-Sweep算法
- 只有在多CPU环境下才有意义
- 使用-XX:+UseConcMarkSweepGC打开
CMS收集器缺点: - CMS以牺牲CPU资源的代价来减少用户线程的停顿。当CPU个数少于4的时候,有可能对吞吐量影响非常大。
- CMS在并发清理的过程中,用户线程还在跑。这时候需要预留一部分空间给用户线程。
- CMS用Mark-Sweep,会带来碎片问题。碎片过多的时候容易频繁触发Full GC。
CMS是基于”标记-清除“算法实现的,这个过程分为4个步骤:
- 初始标记(CMS initial mark)
初始标记只是标记一下GC Roots能直接关联的对象,速度很快。 - 并发标记(CMS concurrent mark)
就是GC Roots Tracing的过程 - 重新标记(CMS remark)
为了修正并发标记标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,当远比并发标记时间短。 - 并发清除(CMS concurrent sweep)
其中,初始标记和重新标记这两个步骤仍然需要 STW
CMS收集器在整个过程中耗时最长的的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
详细过程解析
-
Initial Mark
这是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GC root引用或者被年轻代存活对象所引用的所有对象。
-
Concurrent Mark
在这个阶段Garbage Collector会遍历老年代,然后标记所有存活对象,它会根据上个阶段找到的GC Roots遍历查找。并发标记阶段,它会与用户的应用程序并发运行,并不是老年代所有的存活对象都会标记,因为在并发期间用户的程序可能会改变一些引用。
在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化。
-
Concurrent Preclean
这是一个并发阶段,与应用线程并发运行,并不会stop应用的线程。在并发运行的过程中,一些对象的引用可能会发生变变化,但是这种情况发生时,JVM会将这个对象的区域(Card)标记为Dirty,这也就是Card Marking
在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了。
- Concurrent Abortable Preclean
这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW中最终的标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等) - Final Remark
这个是第二个STW阶段,也是CMS中的最后一个,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。
通常CMS的Final Remark阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能行(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些。
经过以上五个阶段之后,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理那些老年代不再使用的对象。 -
Concurrent Sweep
这里不需要STW,它是与用户的应用程序并发运行,清除那些不再使用的对象回收它们的占用空间为将来使用。
- Concurrent Reset
这个阶段也是并发执行的,它会重置CMS内部数据结构,为下次的GC做准备。
总结
CMS通过将大量工作分散到并发处理阶段来减少STW时间,在这块做得非常优秀,但是CMS也有一些其他的问题。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,可能引发串行Full GC。
空间碎片,导致无法分配大对象,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
对于堆较大的引用,GC的时间难以预估。
G1收集器
- G1收集器是一个面向服务端的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。
- 它满足短时间gc停顿的同时达到一个较高的吞吐量。
- JDK7以上版本适用
设计目标
- 与应用线程同时工作,几乎吧需要stop the world(与CMS类似)
- 整理剩余空间,不产生内存碎片(CMS只能在Full GC时,用stop the world整理内存碎片)
- GC停顿更加可控
- 不牺牲系统的吞吐量
- gc不要求额外的内存空间(CMS需要预留空间存储浮动垃圾)
G1设计规划 要替换掉CMS
G1在某些方面弥补了CMS的不足, 比如CMS算法使用的是mark-sweep算法,自然会产生内存碎片;然而G1基于copying算法,高效的整理剩余内存,而不需要管理内存碎片。
另外,G1提供了更多手段,以达到对gct停顿时间的可控。
G1收集器堆的结构
- head被划分为一个个相等的不连续的内存区域(region),每个region都有一个分代角色:eden、survivor、old
- 对每个角色的数量并没有强制限定,也就是说对每种分代的内存大小,可以动态变化。
- G1最大的特点就是高效地执行回收,优先去执行那些大量对象的可回收区域。
- G1使用了gc停顿可预测模型,来满足用户设定gc停顿时间,根据用户设定的目标时间,G1会自动的选择哪些region要清除,一次清除多少个region
- G1从多个region中复制存活对象,然后集中放到一个region中,同时整理、清理内存(copying收集算法)
G1的重要概念
- 分区
G1采用了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控等问题--G1将整个堆分成相同大小的分区。每个分区即可能是年轻代也可能是老年代,但在某一时刻只能属于某个代,分代概念还在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。-
在物理上不需要连续,则带来了额外的好处--有的分区垃圾对象很少,有的分区垃圾对象特别多,G1会优先回收垃圾特别多的分区
,这样可以花费较少的时间来回收这些分区的垃圾,也就是G1名字的由来,即首先收集垃圾最多的分区。
依然是在新生代满的时候,对整个新生代进行回收--整个新生代的对象要么被回收、要么被晋升,至于新生代也采取分区机制的原因,则是因为这样更老年代的策略统一,方便调整代的大小。 - 收集集合(CSet)
一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden分区、survivor分区或者老年代。 -
已记忆集合(RSet)
RSet记录了其他Region对象引用本Region中对象的关系,属于points-into结构(谁引用了我了的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区的对象,只扫描RSet即可。
G1 GC是在points-out的card table之上再加了一层结构来构成points-into RSet:每个regionh会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内。
这个RSet其实是一个hash table,key是别的region的起始位置,value是一个集合,里边的元素是card table的index。举个例子来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,他的意思就是region B的一个Card里有引用指向region A。所以对A来说该RSet记录的是points-into的关系,而card table仍然记录points-out的关系。
- Snapshot-At-The-Beginning(SATB)
SATB是G1 GC在并发标记阶段使用的增量式的标记算法。
并发标记是多线程的,但并发线程在同一时刻只扫描一个分区。
G1相对于CMS的优势
- G1在压缩空间方面有优势
- G通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden、Survivor、Old区不再固定,在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause TIme)来控制垃圾收集时间,避免应用雪崩现象
- G1在回收内存后会马上同时做合并空闲内存的工作,而CMS默认是在STW的时候做
- G1会在Young GC中使用,而CMS只能在Old区使用
G1的适合场景
- 服务端多核CPU、JVM内存占用较大的引用
- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
- 向要更可控、可预期的GC停顿周期;防止高并发下应用的雪崩现象
G1 GC模式
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全STW的。
- Young GC:选定所有年轻代里的Region。通过控制年轻代的Region个数,即年轻代内存大小来控制Young GC的时间开销。
- Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region
Mixed GC不是Full GC,它只能回收部分老年代的region, 如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC, 就会使用serial old GC(Full GC)来收集整个GC heap。所以本质上,G1是不提供Full GC的。
Global Concurrent Marking
Global Concurrent Marking的执行过程类似于CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。
Global Concurrent Marking的执行过程分为四个步骤:
- 初始标记(initial mark,STW):它标记了从GC Root开始直接可达的对象。
共用了Young GC的的暂停,这是因为它们可以服用root scan操作,所有可以说global concurrent marking是伴随Young GC而发生的。 - 并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
- 重新标记(Remark, STW):标记那些在并发标记阶段发生变化的对象,将被回收。
- 清理(Cleanup):清除空Region(没有存活对象的),加入到free list。
G1在运行过程中的主要模式
YGC(不同于CMS)
G1 YGC在Eden充满时触发,在回收之后所有之前属于Eden的区块全部变成空白,即不属于任何一个分区。并发阶段
-
混合模式
什么时候会发生Mixed GC
由一些参数控制,另外也控制着哪些老年代Region会被选人CSet(收集集合)。
G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet
G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数
G1OldCSetRegionThresholdPercent:一次Mixed GC中能选入CSet的最多old region数量。
Full GC(一般是G1出现问题时发生)。
Humongous区域
在G1中,还有一种特殊的区域,叫Humongous区域。如果一个对象占用的空间达到或是超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humougous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1 Young GC
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC完成工作,引用线程继续执行。
如果仅仅GC新生代对象,我们如何找到所有的根对象呢?老年代的所有对象都是根吗?那么这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
由于新生代有多个,那么我们需要在新生代之间记录引用吗,这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
- 根扫描
静态和本地对象被扫描 - 更新RS
处理dirty card队列更新RS - 处理RS
检测从年轻代指向老年代的对象 - 对象拷贝
拷贝存活的对象到Survivor/old区域 - 处理引用队列
软引用,弱引用,虚引用处理
三色标记算法
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有效的方法,利用它可以推演回收器的正确性。
- 黑色:根对象,或者该对象与它的子对象都被扫描过的
- 灰色:对象本身被扫描,但还没扫描完该对象的子对象
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象。
当 GC 开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色:
继续由灰色遍历,将已扫描了子对象的对象置为黑色:
遍历了所有可达的对象后,所有可达的对象都变成了黑色;不可达的对象即为白色,需要被清理:
但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题。
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
A.c = C
B.c = null
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会成下图这样:
很显然,此时 C 是白色的,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC 标记的对象不丢失呢?有如下两种可行的方式:
- 在插入的时候记录对象
- 在删除的时候记录对象
在 G1 中,使用的是 SATB(Snapshot-At-The-Beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
- Step-1:在初始标记的时候,生成一个快照图,用于标记存活对象。
- Step-2:在并发标记的时候,所有被改变的对象入队(在 Write Barrier 里把所有旧的引用所指向的对象都变成非白的)。
- Step-3:可能存在游离的垃圾,将在下次被收集。
这样,G1 到现在可以知道哪些老的分区可回收的垃圾最多。当全局并发标记完成后,在某个时刻,就开始了 Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式 GC 也是采用的复制的清理策略,当 GC 完成后,会重新释放空间。