HotSpot JVM中GC的实现主要有以下的几种:
- Serial/Serial Old
- ParNew
- Parallel Scavenge/Parallel Old
- Concurrent Mark Sweep(CMS)
- Garbage First(G1)
分别简单总结一下。
Serial/Serial Old
Serial 收集器是最基本的、历史最悠久的收集器。从字面上就能看出,这是一个单线程的收集器,即在进行GC时必须STW。
Serial收集器在新生代采用复制算法,在老年代采用标记-清理-压缩算法(Serial Old)。
ParNew
ParNew收集器是Serial收集器的多线程版本,采用多线程进行收集,但一样要STW。
与Serial类似,ParNew收集器在新生代采用复制算法,在老年代采用标记-清理-压缩算法。
ParNew比较重要,因为它可以配合CMS收集器一起使用(Parallel Scavenge则不行)。ParNew是-XX:+UseConcMarkSweepGC
选项下默认的新生代收集器。
Parallel Scavenge
Parallel Scavenge是一个新生代收集器,它与ParNew最主要的区别是它的目标是吞吐量优先而不是时间优先(注意这两个不能兼得)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU运行总时间的比值。吞吐量优先适合在后台完成计算而不需要太多交互的业务,而时间优先适合需要交互和实时性的业务。
Parallel Scavenge可以精确控制吞吐量,通过两个参数:控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis
参数及直接设置吞吐量大小的-XX:GCTimeRatio
参数。它还可以通过打开-XX:+UseAdaptiveSizePolicy
参数进行自适应调节(GC Ergonomics),打开后JVM会根据当前运行状况收集监控信息并动态调整参数来提供最合适的吞吐量,配合前两个参数使用更好。
Parallel Old
Parallel Old是Parallel Scavenge对应的老年代版本,目标也是吞吐量优先,可以与Parallel Scavenge结合。
Concurrent Mark Sweep
CMS是真正意义上的并发收集器,作用于老年代。CMS的目标是时间优先(最短停顿时间),像服务器之类的就很适合跑在CMS收集器下,因为互联网服务重视服务的响应速度,希望系统延迟时间短。CMS通常与ParNew配合使用。
CMS的过程
CMS是基于标记-清除算法实现的,整个过程分几步:
- 初始标记(initial-mark):从GC Root开始,仅扫描与根节点直接关联的对象并标记,这个过程需要STW,但是GC Root数量有限,因此时间较短
- 并发标记(concurrent-marking):这个阶段在初始标记的基础上继续向下进行遍历标记。这个阶段与用户线程并发执行,因此不停顿
- 并发预清理(concurrent-precleaning):上一阶段执行期间,会出现一些刚刚晋升老年代的对象,该阶段通过重新扫描减少下一阶段的工作。该阶段并发执行,不停顿
- 重新标记(remark):重新标记阶段会对CMS堆上的对象进行扫描,以对并发标记阶段遭到破坏的对象引用关系进行修复,以保证执行清理之前对象引用关系是正确的。这一阶段需要STW,时间也比较短暂
- 并发清理(concurrent-sweeping):清理垃圾对象,这个过程与用户线程并发执行,不停顿
- 并发重置(reset):重置CMS收集器的数据结构,等待下一次GC
可以看到,整个过程中需要STW的阶段仅有初始标记和*重新标记阶段,所以可以说它的停顿时间比较短(当然吞吐量可能会受影响)。
CMS的缺陷
由于CMS是基于 标记-清理 算法的,因此会产生大量的内存碎片。这很可能会出现老年代虽然有大量不连续的空闲内存,但很难找到连续的内存空间来给对象分配,不得不提前触发一次Full GC的情况。针对这一点,CMS提供了一个-XX:+UseCMSCompactAtFullCollection
开关(默认开启),用于在CMS要gg的时候进行内存碎片整理从而得到连续的内存空间。这样内存碎片的问题可以解决,但STW的时间也相应变长。
另外,CMS收集器无法处理 浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留待下一次GC时再将其清理掉,这一部分垃圾就称为“浮动垃圾”。由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了92%的空间后就会被激活(JDK 1.6)。可以通过设置-XX:CMSInitiatingOccupancyFraction
的值来改变这个阈值。注意一定要结合实际的运行情况,不要设的太大,假如内存真的太满,CMS要gg的时候就会临时召唤出Serial Old对老年代进行Full GC,停顿时间长,因此一定要合理设置这个参数的值。
日志分析
我们可以通过日志观察一次完整的CMS GC过程(参数:-XX:+UseConcMarkSweepGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
):
|
|
【关于CMS-concurrent-abortable-preclean】:从日志中我们还发现了一个细节叫做CMS-concurrent-abortable-preclean
,这就要从Concurrent precleaning阶段说起了。Concurrent precleaning阶段的实际行为是:针对新生代做抽样,等待新生代在某个时间段(默认5秒,可以通过CMSMaxAbortablePrecleanTime
参数设置)执行一次Minor GC,如果这个时间段内GC没有发生,那么就继续进行下一阶段(Remark);如果时间段内触发了Minor GC,则可能会执行一些优化(具体可以参考https://blogs.oracle.com/jonthecollector/entry/did_you_know)
G1
G1(Garbage First)收集器是HotSpot JVM最新的垃圾收集器,它最大的特点就是将堆内存划分成多个连续的区域(region),每个区域大小相等。因此在G1中新生代与老年代都是由若干个Region组成(不需要连续)。Region的大小是可以重新设置的。
G1的优点:可以非常精确地控制停顿;老年代采用标记-压缩算法,避免了内存碎片的问题。
G1会在内部维护一个优先列表,通过一个合理的模型,计算出每个Region的收集成本和收益期望并量化,这样每次进行GC时,G1总是会选择最适合的Region(通常垃圾比较多)进行回收,使GC时间满足设置的条件。
G1新生代GC过程
G1的新生代收集类似于ParNew,同样是基于复制的算法(英文叫evacuation),存活的对象会被移至Survivor区,空间不够则一些对象需要晋升至老年代。新生代收集同样会有STW。
每次GC中,Eden区和Survivor区的大小都会被重新计算来提供给下一次Minor GC(根据内部记录的一些信息以及设置的期望停顿时间)。
Remembered Set
G1通过引入Remembered Set来避免全堆扫描。Remembered Set用于跟踪对象引用。G1中每个Region都有对应的Remembered Set。当JVM发现内部的一个引用关系需要更新(对Reference类型进行写操作),则立即产生一个Write Barrier中断这个写操作,并检查Reference引用的对象是否处于不同的Region之间(用分代的思想,就是新生代和老年代之间的引用)。如果是,则通过CardTable通知G1,G1根据CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中,并将Remembered Set加入GC Root。这样,在G1进行根节点枚举时就可以扫描到该对象而不会出现遗漏。
G1老年代GC过程
一共六步,直接放上官方的解释:
Initial Mark(STW)
This is a stop the world event. With G1, it is piggybacked on a normal young GC. Mark survivor regions (root regions) which may have references to objects in old generation.
Root Region Scanning
Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur.
Concurrent Marking
Find live objects over the entire heap. This happens while the application is running. This phase can be interrupted by young generation garbage collections.
Remark(STW)
Completes the marking of live object in the heap. Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector.
Cleanup(STW and concurrent)
- Performs accounting on live objects and completely free regions. (Stop the world)
- Scrubs the Remembered Sets. (Stop the world)
- Reset the empty regions and return them to the free list. (Concurrent)
Copying(STW)
These are the stop the world pauses to evacuate or copy live objects to new unused regions. This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)].
总结
G1老年代收集的几个要点:
- Concurrent Marking Phase
- Liveness information is calculated concurrently while the application is running.
- This liveness information identifies which regions will be best to reclaim during an evacuation pause.
- There is no sweeping phase like in CMS.
- Remark Phase
- Uses the Snapshot-at-the-Beginning (SATB) algorithm which is much faster then what was used with CMS.
- Completely empty regions are reclaimed.
- Copying/Cleanup Phase
- Young generation and old generation are reclaimed at the same time.
- Old generation regions are selected based on their liveness.
日志分析
分析日志可以清晰地观察G1的收集阶段(JVM参数-XX:+UseG1GC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
):
目前来说,大内存对G1支持的较好,其余的情况有待观察。是否将GC替换为G1还有待实验(何况很多公司还在用JDK 1.7)。有消息称JDK 9会把G1作为默认的GC。
其他的JVM GC实现
关于低延时的GC实现,Azul的C4: The Continuously Concurrent Compacting Collector做的挺好的,STW时间非常低,而且对超大堆支持的很好(据说能支持2T的内存。。),当然吞吐量会稍弱。
参考资料
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明 著
- Garbage Collection Understanding Java, Azul
- Getting Started with the G1 Garbage Collector
- Garbage-First Garbage Collection, David Detlefs, Christine Flood, Steve Heller, Tony Printezis, Sun Microsystems, Inc.
- Jon Masamitsu’s Weblog - an article about CMS