垃圾收集器
垃圾收集器是垃圾收集相关算法的实践者,《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含 的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特 点和要求组合出各个内存分代所使用的收集器。
上图所示的是七种作用于不同分代的垃圾回收器,它们之间有的两两配合使用。并没有一款垃圾收集器可以完美解决一切问题,而是每一种情况都有一个合适的垃圾收集器,而HotSpot就实现了很多种类的垃圾收集器。
后面会涉及到并行、并发的垃圾收集器,所以应该清楚相关的概念:
- 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
- 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
吞吐量计算公式:
吞吐量 = 运 行 用 户 代 码 时 间 运 行 用 户 代 码 时 间 + 运 行 垃 圾 收 集 时 间 运行用户代码时间\over运行用户代码时间+运行垃圾收集时间 运行用户代码时间+运行垃圾收集时间运行用户代码时间
GC分类与性能指标
GC分类
从不同角度分,垃圾收集器可以分为多种。
按线程数分(垃圾回收线程数),可以分为 串行垃圾回收器 和 并行垃圾回收器 。

- 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
- 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以, 串行回收默认被应用在客户端的Client模式下的JVM中。
- 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。
- 和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。
按照工作模式分,可以分为 并发式垃圾回收器和独占式垃圾回收器 。
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器(Stop the World)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。再分配对象空间使用指针碰撞
- 非压缩式的垃圾回收器不进行这步操作,分配对象空间使用空闲列表
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
- 吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
- 这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。
- 简单来说,主要抓住两点:
- 吞吐量
- 暂停时间
吞吐量(throughput)
- 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)- 比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
- 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的
- 吞吐量优先,意味着在单位时间内,STW的时间最短:0.1+0.2=0.3

暂停时间(pause time)
- “暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态。
- 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的
- 暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5,但是总的GC时间可能会长

总结 :
- 高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。
- 低暂停时间(低延迟)较好,是从最终用户的角度来看,不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。因此,具有较低的暂停时间是非常重要的,特别是对于一个交互式应用程序(就是和用户交互比较多的场景)。
- 不幸的是 ”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。
- 因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
- 相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。
- 在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。
- 现在标准:在最大吞吐量优先的情况下,降低停顿时间
Serial收集器 (串行回收)
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。从名字就可以看出,Serial收集器是一个单线程的垃圾收集器:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
- Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。
- Serial收集器采用复制算法、串行回收和”Stop-the-World”机制的方式执行内存回收。
- 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial old收集器同样也采用了串行回收和”Stop the World”机制,只不过内存回收算法使用的是标记-压缩算法。
- Serial Old是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用②作为老年代CMS收集器的后备垃圾收集方案

Serial收集器的优势
- 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client模式下的虚拟机是个不错的选择。
- 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
- 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
- 等价于新生代用Serial GC,且老年代用Serial Old GC
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。
它也可能有另外两种用途:
- 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用。
- 另一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
总结:
Serial收集器适合应用于Client模式或者单核CPU内存小的情况下,Serial单线程执行垃圾收集,CPU不需要来回切换时间片,在前面的特定情况下简单而高效。Serial在年轻代采用标记-复制算法,在老年代采用标记-整理算法。
ParNew收集器 (并行回收)
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集 器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。
在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次 实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者 Serial 收集器中的一个。ParNew 收集器是激活CMS后(使用
-XX:+UseConcMarkSweepGC选项)的默 认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
总结ParNew收集器
- ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。
- ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
- 随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时 系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多(譬如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常普遍)的环境中,可以使用
-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 - 除Serial外,目前只有ParNew GC能与CMS收集器配合工作。
Parallel收集器 (吞吐量优先)
Paralle 并行 Scavenge 收集
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
从上面这句话来看,Parallel收集器和ParNew收集器有着很多相似之处:都是能够并行收集的多线程收集器。
那么它有什么特点呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间 (低延迟) ,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
吞吐量 = 运 行 用 户 代 码 时 间 运 行 用 户 代 码 时 间 + 运 行 垃 圾 收 集 时 间 运行用户代码时间\over运行用户代码时间+运行垃圾收集时间 运行用户代码时间+运行垃圾收集时间运行用户代码时间
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。但是不要设置的过于小,因为垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的,设置的太小会使收集频率变高,从而降低吞吐量。-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。-XX:+UseAdaptiveSizePolicy。这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
这个收集器是直到 JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一 定比ParNew加CMS的组合来得优秀。

CMS收集器 (低延迟)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

CMS 收集步骤
CMS: Concurrent Mark Sweep中Mark Sweep 就可以看出CMS是基于标记-清除算法实现的,其运作原理较为复杂,整个过程包括四个步骤:
- 初始标记(CMS Initial-Mark) 阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
- 并发标记(CMS Concurrent-Mark) 阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(CMS Remark) 阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World”的发生,但也远比并发标记阶段的时间短。
- 并发清除(CMS Concurrent-Sweep) 阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
过程图:

CMS的三个主要缺点
- CMS收集器对处理器资源非常敏感。 事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
- CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。并且由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数
-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动 阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。"浮动垃圾"的说明:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运。
- CMS是一款基于
标记-清除算法实现的收集器, 这意味着收集结束时会有大量空间碎片产生。 空间碎片过多时,将会给大对象分配带来很**烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题, CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。
JDK 后续版本中 CMS 的变化
- DK9新特性:CMS被标记为Deprecate了(JEP291)
- 如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
- JDK14新特性:删除CMS垃圾回收器(JEP363)移除了CMS垃圾收集器
- 如果在JDK14中使用XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动回退以默认GC方式启动JVM。
总结
- CMS是一个里程碑,第一款可以垃圾回收线程和用户线程可以并发工作的垃圾回收器。
- 只有CMS中存在Major GC行为,也就是单独回收老年代的行为。
- CMS采用
标记-清除算法,会产生内存碎片。 - 无法处理浮动垃圾(在并发清理中用户线程产生的垃圾)。
- 不会等到老年代空间慢了在进行GC操作,而是达到一定阈值(默认值是JDK5是老年代使用了68%,JDK6增长为92%)就进行GC操作。
- 如果CMS运行期间预留的内存空间无法满足分配对象用的空间,就会出现一次“并发失败”(Concurrent Mode Failure),这时虚拟机就会启动后备方案: 冻结用户线程的执行,临时启动Serial Old收集器重新收集老年代的垃圾,导致停顿时间加长。
Garbage First收集器 (区域化分代式)
G1的特点
- G1也遵循分代收集理论,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
- 这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。
- G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
- 在JDK1.7版本正式启用,移除了Experimental的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为全功能的垃圾收集器。
- 与此同时,CMS已经在JDK9中被标记为废弃(deprecated)。G1在JDK8中还不是默认的垃圾回收器,需要使用
-XX:+UseG1GC来启用。 - G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不同的条件下被触发。
G1的优势
采用了全新的分区算法。
- 并行与并发兼备
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
- 分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代。
- 空间整合
- CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
G1的缺点
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
- 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1参数设置
-XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务-XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。-XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标,JVM会尽力实现,但不保证达到。默认值是200ms-XX:+ParallelGCThread:设置STW工作线程数的值。最多设置为8-XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。-XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
G1的适用场景
- 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
- 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
- 用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:
- 超过50%的Java堆被活动数据占用;
- 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于0.5至1秒)
- HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
可预测的停顿时间模型
可预测的停顿时间模型(即:软实时soft real-time)
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
记忆集与卡表
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。
记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些下面列举了一些可供选择的记录精度:
- 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
第三种 “卡精度” 所指的是用一种称为 卡表(Card Table) 的方式去实现记忆集,这也是目前最常用的一种记忆集实现形式。但是不要将卡表与记忆集合直接混为一谈,记忆集是一种更抽象的数据结构,抽象定义了记忆集的行为和意图,没有定义实现;而卡表则是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。(可以按照Map与HashMap的关系来类比理解)。
卡表的最简单的形式可以是一个字节数组,HotSpot就是采用的这种方式:CARD_TABLE [this address >> 9] = 0;
- 字节数组
CARD_TABLE也就是卡表,其中的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个
内存块被称作 “卡页”(Card Page)。卡页大小一般都是以2的N次幂的字节数。HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。 - 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。 在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
分区(Region)
- G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
- 收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
- 每个Region的大小可以通过参数
-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。 - Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个
Region容量一半的对象即可判定为大对象。 - 而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待。
- 每个Region都是通过指针碰撞来分配空间。
G1收集器Region分区示意图:

G1将堆内存“化整为零”的“解题思路”听起来简单易懂,但是想要实现的细节却十分困难。
如,将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?
- 使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
- G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
- 首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构。
- CMS收集器采用增量更新算法实现,而G1 收集器则是通过原始快照(SATB)算法来实现的。(相关详情可以参考《深入理解Java虚拟机》3.4.6节)
- 垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
- G1收集器默认在 这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中 的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度, G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
怎样建立起可靠的停顿预测模型?
用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?
G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。 这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
G1的运作过程
如果不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作)G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1回收过程一:初始标记过程
- JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。
- 年轻代回收只回收Eden区和Survivor区。
- YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
如下图所示:

之后开始下面的过程:
- 第一阶段,扫描根
- 根是指GC Roots,根引用连同RSet记录的外部引用作为扫描存活对象的入口。
- 第二阶段,更新RSet
- 第三阶段,处理RSet
- 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
- 第四阶段,复制对象。
- 此阶段,对象图被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象
- 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
- 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
- 第五阶段,处理引用
处理
Soft,Weak,Phantom,Final,JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
补充:
- 对于应用程序的引用赋值语句 oldObject.field(这个是老年代)=object(这个是新生代),JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
- 那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。
G1回收过程二:并发标记过程
- 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。正是由于该阶段时STW的,所以我们只扫描根节点可达的对象,以节省时间。
- 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在Young GC之前完成,因为Young GC会使用复制算法对Survivor区进行GC。
- 并发标记(Concurrent Marking):
- 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。
- 在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
- 同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
- 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的原始快照算法:Snapshot-At-The-Beginning(SATB)。
- 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集。
- 并发清理阶段:识别并清理完全空闲的区域。
G1回收过程三:混合回收过程
当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。

细节分析:
- 并发标记结束以后,老年代中全为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过
-XX:G1MixedGCCountTarget设置)被回收。意思就是一个Region会被分为8个内存段。 - 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
- 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收。
XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。 - 混合回收并不一定要进行8次。有一个阈值
-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
G1 可选回收过程:Full GC
- G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
- 要避免Full GC的发生,一旦发生Full GC,需要对JVM参数进行调整。什么时候会发生FullGC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。
导致G1 Full GC的原因可能有两个:
- Evacuation的时候没有足够的to-space来存放晋升的对象;
- 并发处理过程完成之前空间耗尽。
G1 收集器的优化建议
- 年轻代大小设置问题
- 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小,因为固定年轻代的大小会覆盖可预测的暂停时间目标。我们让G1自己去调整
- 暂停时间目标不要太过严苛
- G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
- 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/20118.html