新生代区垃圾收集器
1丶Serial收集器
Serial收集器是最基础丶发展历史最悠久的收集器,在JDK 1.3之前是JVM 新生代区垃圾收集的唯一选择。Serial收集器是单线程的串行进行垃圾收集的收集器,而且,它在进行垃圾收集是必须要暂停所有的工作线程(Stop The World 简称STW),直到垃圾收集结束。

注:看到这里,大家可能觉得Serial收集器是不是完全没用,但其实到现在,它依然是虚拟机运行在client模式下默认新生代区收集器。它也有优于其他收集器的地方,特别是单个CPU的环境来说,它有一个很明显的优点:简单而高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集,自然可以获得最高的单线程收集效率。
2丶ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数丶收集算法丶Stop The World丶对象分配规则丶回收策略等都与Serial收集器完全一样。在hotspot的具体实现中,它们也共用了很多代码。

注:收集器在单CPU的环境下绝对不会有比Serial收集器更好的效果,是由于线程交互需要开销。当然,随着可用的CPU的数量增加,他对于GC时系统资源的有效利用还是很有好处的。
3丶Parallel Scavenge收集器
Parallel Scavenge是一款基于复制算法的新生代区垃圾收集器,它的垃圾收集也是并行的多线程收集器,它和ParNew收集器的区别,ParNew的相关优化的重点都是为了尽可能的缩短STW时间,但是Parallel Scavenge则是为了达到一个可控制的吞吐量,提高了吞吐量,也就可以更高效率的利用CPU。
注:吞吐量 = 运行时间 / (运行时间 + 垃圾收集时间)
Paraller Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。大家不要认为把停顿时间设置小一点就能保证系统的垃圾收集变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:新生代调小了,小的空间当然会比大的空间收集快,当是同时也会导致垃圾收集发生的更频繁。
Paraller Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy这是个开关参数,当打开时,就不需要手动指定新生代的大小丶Eden与Survivor区的比例丶晋升老年代对象大小等,虚拟机会根据当前系统运行情况收集性能 监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应的调节策略。
老年代区垃圾收集器
1丶Serial Old收集器
Serial Old是Serial收集器的老年代收集器版本,采用标记-整理算法,也是一个单线程的垃圾收集器。
注:在Server模式下,它主要还有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallerl Scavvenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
2丶Parallel Old收集器
Parallel Old收集器是Parallel Scavenge的老年代收集器版本,同样采用标记-整理算法,和Serial Old相比,它是一个多线程的垃圾收集器。
注:该收集器是在JDK 1.6之后才提出的,在此之前,Parallel Scavenge只能与Serial Old搭配使用,由于Serial Old性能较低,所以就算使用Parallel Scavenge也并不能在整体上提升吞吐量。Parallel Old提出后该问题就迎刃而解了,在注重吞吐量的场合,可以考虑使用Parallel Scavenge + Parallel Old组合。
3丶CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于标记-清除算法实现的。

运行过程主要分为以下四个部分:
| 过程 | 内容 |
|---|---|
| 初始标记(CMS initial mark) | 标记一下GC Roots能直接关联到的对象,速度很快。(需要STW) |
| 并发标记(CMS concurrent mark) | 进行GC RootsTracing的过程。 |
| 重新标记(CMS remark) | 为了修正并发标记期间因用用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。(需要STW) |
| 并发清除(CMS concurrent sweep) | 回收的可回收对象。 |
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
- CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量 + 3) / 4, 当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度降低。为了应付这种情况,虚拟机提供一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/ i-CMS)的CMS收集器变种,所做的事情和单CPU操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记丶清除的时候让GC线程丶用户线程交替运行,尽量减少GC线程的独占资源的时间,虽然会增加垃圾收集的时间,但是会减少用户程序的影响,程序执行速度下降就没那么明显。实践证明,i-CMS收集器效果很一般,不提倡用户使用。
- CMS收集器无法处理浮动垃圾(由于CMS并发清理阶段用户线程还在运行着,伴随程序运行生产新的垃圾,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留到下一次GC时再清理。这一部分垃圾就称为“浮动垃圾”),可能出现“Concurrent Mode Failure”失败而导致另外一次Full GC的产生。由于在垃圾收集阶段用户线程还需要运行,需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能等待老年代几乎填满再进行收集。在JDK 1.5中,CMS收集器当老年代使用了68%的空间就会被激活。在JDK1.6中,CMS收集的启动阈值提升到92%。(可使用-XX:CMSInitiatingOccupancyFraction参数设置触发的百分比,参数设置太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。)要是CMS运行期间预留的内存无法满足程序需要就会出现一次“Concurrent Mode Failure”失败,这是虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
- CMS是基于标记-清除算法的垃圾收集器,这就意味着收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了-XX:+UseCMSCompactFullCollection参数(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没了,但停顿时间变长了,虚拟机还提供了另一个参数-XX:+UseCMSFullGCsBeforeCompaction,用于设置执行多少次不压缩Full GC后,跟着来一次带压缩的(默认值为0.表示每次都进行压缩)
G1收集器
G1收集器是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是可以替换掉CMS收集器。

运行过程和CMS有很多相似,大致可分为以下四个部分:
| 过程 | 内容 |
|---|---|
| 初始标记(Initial Marking) | 标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,速度很快。(需要STW) |
| 并发标记(Concurrent Marking) | 进行GC RootsTracing的过程。 |
| 最终标记(Final Marking) | 为了修正在并发标记其他因用户程序运作而导致标记产生变动。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set 中。(需要STW,但是可并行执行) |
| 筛选回收(Live Date Counting and Evacuation) | 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(需要STW,但是可以与用户线程一起并发执行,停顿用户线程是为了大幅提高收集效率) |
与其他GC收集器相比,G1具备如下特点:
- 并行与并发:G1能充分利用多CPU丶多核环境下的硬件优势,使用多个CPU来缩短“Stop-The-World”停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集:与其他收集器一样,分代概念在G1依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间丶熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的别一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。G1收集器,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的收集收集。G1跟踪各个Region里面的垃圾堆积大价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
下面这部分内容,具体可以看下面补充的跨代引用
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代的列子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,则通过Card Table把相关引用信息记录到被引用对象所属的Region的Remembered Set中。
补充
1丶CMS丶G1的并发标记
像Serial、Parallel之类的收集器,无论是单线程和多线程标记,本质采用的是暂停用户线程进行标记的算法,优点是实现简单,缺点就是标记时间长,导致STW的时间很长。而CMS和G1,采用的是并发标记,可以在不暂停用户线程的情况下对进行标记,实现这种并发标记的算法就是三色标记法。
三色标记
- 白色:表示这个对象没有被垃圾收集器访问过。
- 灰色:表示这个对象被垃圾收集器访问过,但是它存在一些引用还没被扫描过。
- 黑色:表示这个对象被垃圾收集器访问过了,并且它的所有引用也都被扫描过,它绝对是存活的。

并发过程中产生的问题

由于并发标记的过程中,用户线程并不会暂停,在标记的过程中,用户线程的操作可能会对某些对象的引用进行修改,如上图那样,白色对象明明是存活的,最终还是被回收了。
对象消失问题:扫描的过程中插入一条或者多条从黑色对象到白色对象的新引用,并且去掉了灰色对象到该白色对象的直接引用或者间接引用。
解决方案:只需破坏这两个条件任意一个即可,CMS解决的方案是增量更新,G1解决的方案是原始快照。
增量更新 (Incremental Update)(CMS处理的方案)当黑色对象新插入指向白色对象的新引用的时候,把这个新增引用关系记录下来,到了重新标记的阶段,再根据记录重新扫描一遍。
原始快照( Snapshot At The Begining : SATB)(G1处理的方案)当灰色对象删除掉到指向白色对象的引用的时候,把这个删除引用关系记录下来,到了最终标记的阶段,再根据记录重新扫描一遍。
2丶跨代引用
跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用。

如果新生代发生了垃圾回收的话,就不得不遍历整个老年代,但是这样做代价太多了,极大的浪费性能。那有什么解决方案呢?
记忆集(Remembered Set):一种数据结构,用于记录从非收集区域指向收集区域的指针集合。
根据记录精度分三类:
字长精度:记录精确到一个机器字长。
对象精度:记录精确到一个对象。
卡精度:每个记录精确到一块内存区域。
卡表(Card Table):它是记忆集的一种实现。以第三种卡精度实现的记忆集,也是目前最常用的方式。记忆集是抽象的概念,而卡表就是记忆集的一种具体实现。
卡表的实现:卡表是基于数组实现的:CARD_TABLE[this addredd >>9]=0,每个元素对应着非收集区域中的一块内存区域,称为“卡页”。HotSpot默认的卡页大小为2^9,即512字节,只要该内存区域发生跨代引用,对应的卡页的值就标为1,否则就标为0。

卡表的维护:简单来说,当老年代对象对新生代对象引用关系发生变化,卡表也要发生变化。那卡表的变化是通过什么来实现的呢?HotSpot通过写屏障技术来维护卡表状态。
写屏障:写屏障可以看做在虚拟机层面对“引用类型字段赋值”动作的AOP切面,在赋值时会产生一个环形通知。赋值前称为“写前屏障”(Pre-Write Barrier),赋值后称为“写后屏障”(Post-Write Barrier)。
参考文献:《Java虚拟机规范》丶《深入理解Java虚拟机》

本文详细介绍了Java虚拟机的垃圾收集器,包括Serial、ParNew、ParallelScavenge、SerialOld、ParallelOld、CMS和G1。CMS与G1着重于减少停顿时间,采用并发标记算法,解决全堆扫描问题。CMS使用三色标记法应对并发过程中可能出现的对象消失问题,而G1则通过原始快照和增量更新来处理。此外,文章还讨论了跨代引用和记忆集的概念,以及如何通过写屏障技术维护卡表。
1099

被折叠的 条评论
为什么被折叠?



