HotSpot JVM中的垃圾回收机制【Garbage collection in HotSpot JVM】

翻译一些JVM相关的文章,原文地址:http://blog.ragozin.info/2011/12/garbage-collection-in-hotspot-jvm.html
理解比较浅显,仅供参考,如有翻译错误欢迎留言纠正。

在这个博客中你会发现一些关于Oracle HotSpot和其他一些JVM的垃圾回收机制的文章,这些文章比较深入。但是如果你是垃圾回收机制方面的新手,你可能会对于某些细节层面的内容感到头晕。所以我认为,在更高层面来研究GC模型,可以对你已有的基础知识做一个很好的补充。

 

前言

Java(包括其他主流现代开发语言),以拥有自动内存管理又称垃圾回收为特色。一旦对象的实例变成应用程序不可读状态,它就会被标记为垃圾,最终占用的内存被释放。

一个最简单接触自动内存管理的方法是引用计数(reference counting)。但是这有个严重的限制,它无法处理对象图中的循环链接(cyclic links)。

如果我们想彻底找到所有不可达对象,唯一的方法就是,通过递归整个对象图,找到所有可达的对象。听起来是个很复杂的操作,实际上也是。

在现代JVM包括Oracle HotSpot中,垃圾回收是个漫长进化的产物。人们投入了大量精力和时间,以使其达到了目前的高效性。尽管可能它包含缺陷和不足,但是大多数是由于各种各样原因的折中解决方案。所以请不要轻易指责JVM垃圾回收又慢又笨拙,通常你低估了它实现的复杂性。

好,接下来就开始介绍2011年左右的垃圾回收机制。

 

2011年左右的HotSpot JVM

 

HotSpot有很多垃圾回收模式,通过JVM命令行参数来控制。默认垃圾回收模式取决于JVM的版本,C/S模式以及运行的硬件。(JVM通过试探启发来区分服务器和桌面硬件等级)

 

串行GC

 

JVM参数:-XX:+UseSerialGC

串行GC是区分“代”的垃圾回收算法(如果想知道什么是“代”,请阅读这里)。通常HotSpot中的所有GC模式都是使用分代的处理方法,在每一种模式里就不再重复说明了。

年轻代(young)收集使用复制的方式,而年老代(old space)通过MSC(mark/sweep/compact标记-清除-压缩)算法实现。年轻代和年老代收集都需要暂停整个应用(stop-the-world),而且符合这个收集器的名字,即只使用单线程处理。在年老代收集时,所有活动对象都被移动到这个区域的前面部分。JVM便可以将未使用的内存释放给操作系统。

 

你可以通过-XX:+PrintGCDetails参数,来打开GC日志,就能看到以下GC暂停日志:

年轻代收集

41.614 [GC 41.614: [DefNew:130716K-<7953K(138240K), 0.0525908 secs] 890546K-<771614K(906240K),0.0527947 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]

全量(年轻代+年老代+Perm区)收集

41.908 [GC 41.908: [DefNew:130833K-<130833K(138240K), 0.0000257 secs]41.909: [Tenured:763660K-<648667K(768000K), 1.4323505 secs] 894494K-<648667K(906240K),[Perm : 1850K-<1850K(12288K)], 1.4326801 secs] [Times: user=1.42 sys=0.00,real=1.43 secs]

 

并行清除(parallel scavenge

JVM参数:-XX:+UseParallelGC

垃圾收集的某些阶段,可以很自然的通过多个线程并行化。并行处理可以通过占用多个CPU,减少垃圾收集所需时间,也可以降低整个应用暂停(stop-the-world)的时间。多处理器和处理器多核心的大规模使用,使得并行化成为现代虚拟机必须的特性。

并行清理GC模式,使用年轻代垃圾收集算法的并行实现,而年老代仍使用一个线程来收集。因此,使用这个模式可以降低年轻代垃圾收集暂停时间(更频繁),但是仍然需要忍受长时间全量收集的引起的应用暂停。

并行收集的日志输出例子:

年轻代收集:

59.821: [GC [PSYoungGen:147904K-<4783K(148288K)] 907842K-<769258K(916288K), 0.2382801 secs][Times: user=0.31 sys=0.00, real=0.24 secs]

全量收集:

60.060: [Full GC [PSYoungGen:4783K-<0K(148288K)] [PSOldGen: 764475K-<660316K(768000K)]769258K-<660316K(916288K) [PSPermGen: 1850K-<1850K(12288K)], 1.2817061secs] [Times: user=1.26 sys=0.00, real=1.28 secs]

 

年老代并行GC

JVM参数:-XX:+UseParallelOldGC

这个模式是并行清除的改进版本,增加了年老代并行(并行MSC标记-清除-压缩算法)处理收集。年轻代使用和上一种相同的算法。年老代仍然需要相当长暂停时间,但是调用多核可以使这个时间相对短一些。和串行MSC不同,并行版本不能在堆的尾部创建单个连续空闲内存,所以在这个模式下,JVM无法在全量垃圾回收后将内存释放给操作系统。

并行年老代收集的日志输出例子:

年轻代收集

65.411: [GC [PSYoungGen:147878K-<5434K(144576K)] 908129K-<770314K(912576K), 0.2734699 secs][Times: user=0.41 sys=0.00, real=0.27 secs]

全量收集

65.685: [Full GC [PSYoungGen:5434K-<0K(144576K)] [ParOldGen: 764879K-<623094K(768000K)]770314K-<623094K(912576K) [PSPermGen: 1850K-<1849K(12288K)], 2.5954844 secs][Times: user=3.95 sys=0.03, real=2.60 secs]

 

自适应空间策略

JVM参数:-XX:+UseAdaptiveSizePolicy

这是并行清除(parallel scavenge)收集中的一种特殊模式,可以动态调整年轻代大小的配置,来适应应用的需求。恕我直言,这个模式并不能带来多少改进。仅供实验,不要用于生产环境。

 

Concurrent mark sweep回收器

JVM参数:-XX:+UseConcMarkSweepGC

上面提到的收集器,通常成为吞吐量收集器。相比之下CMS(Concurrent mark sweep)是一个低延迟收集器。它被设计为最小化全局暂停时间,从而提高应用程序的响应速度。对于年轻代,CMS仍旧使用串行复制收集,或者并行收集(并行的算法和前面提到的并行清除模式很相似,但是两者是完全不同的代码,所以少量配置参数会有不同,例如自适应空间策略在CMS不可用)。

年老代绝大部分都是并行收集。从收集器命名可以看出,CMS是标记清除算法(注意名字里缺少压缩)。CMS在每个年老代收集阶段只需要两个短暂停。但是相对于全局暂停的优异表现,CMS不能压缩(重新整理内存中对象的位置),这使得CMS算法更容易产生碎片。CMS使用了某些小技巧来试图解决碎片问题,但是仍然是个缺点。

CMS日志输出例子:

13.433: [GC [1 CMS-initial-mark: 384529K(768000K)] 395044K(906240K), 0.0045952 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

13.438: [CMS-concurrent-mark-start]

...

14.345: [CMS-concurrent-mark: 0.412/0.907 secs] [Times: user=1.20 sys=0.00, real=0.91 secs]

14.345: [CMS-concurrent-preclean-start]

14.366: [CMS-concurrent-preclean: 0.020/0.021 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

14.366: [CMS-concurrent-abortable-preclean-start]

...

14.707: [CMS-concurrent-abortable-preclean: 0.064/0.340 secs] [Times: user=0.36 sys=0.02, real=0.34 secs]

14.707: [GC[YG occupancy: 77441 K (138240 K)]14.708: [Rescan (non-parallel) 14.708: [grey object rescan, 0.0058016 secs]14.714: [root rescan, 0.0424011 secs], 0.0485593 secs]14.756: [weak refs processing, 0.0000109 secs] [1 CMS-remark: 404346K(768000K)] 481787K(906240K), 0.0487607 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]

14.756: [CMS-concurrent-sweep-start]

...

14.927: [CMS-concurrent-sweep: 0.116/0.171 secs] [Times: user=0.23 sys=0.02, real=0.17 secs]

14.927: [CMS-concurrent-reset-start]

14.953: [CMS-concurrent-reset: 0.026/0.026 secs] [Times: user=0.05 sys=0.00, real=0.03 secs]

Time开头部分是并行部分,CMS和程序并行工作。在这里你可以找到更多关于CMS暂停的信息。

 

CMS失败回退至标记-清除-压缩

557.079: [GC 557.079: [DefNew557.097:[CMS-concurrent-abortable-preclean: 0.010/0.109 secs] [Times: user=0.12sys=0.00, real=0.11 secs]

(promotion failed) :130817K-<130813K(138240K), 0.1401674 secs]557.219: [CMS (concurrent modefailure): 731771K-<584338K(768000K), 2.4659665 secs]858916K-<584338K(906240K), [CMS Perm : 1841K-<1835K(12288K)], 2.6065527secs] [Times: user=2.48 sys=0.03, real=2.61 secs]

这里可以找到更多关于失败的信息。

 

CMS增量模式

JVM参数:-XX:+CMSIncrementalMode

CMS使用多个后台线程进行GC,它们与程序并行运行。这些线程会和应用程序竞争CPU资源。增量模式会限制后台GC线程所占用的CPU时间。如果你只有1-2个物理CPU核心,这个模式将有益于提高应用程序的响应时间。相应的,年老代收集的时间会变得更长,全量收集失败率也会变高。

 

G1垃圾收集器

JVM参数:-XX:+UseG1GC

G1 (garbage first)是在HotSpot JVM中一个新的垃圾收集模式,它在最新版本的JDK6中正式被提及。G1是一个实现了增量版本的标记-清除-压缩的低延迟收集器。G1将堆分为固定大小的区域,并且可以在应用暂停(STW)时只收集其子集(部分收集)。和CMS不同,G1需要在暂停应用(STW)期间进行大部分的工作。增量方法允许G1采取更多的短时间暂停,而非少量的长时间JVM暂停(总暂停时间仍然比并行收集器例如CMS要长)。为了更加精确,G1也使用后台线程同应用程序并发,来进行堆的标记工作(类似于CMS),但是大部分工作仍在应用暂停(STW)期间完成。

G1使用复制收集算法来进行部分收集。因此每次收集都能产生一些完全空闲的区域,这部分内存可以返还给操作系统。

G1同样使用分代准则。一部分区域被划分为年轻代,并相应的应用年轻代的策略。

有很多人宣传G1为“垃圾收集的杀手锏”,但是我个人多次持非常怀疑的态度。下面是一些原因:

1.G1需要维护一些部分收集使用到的、额外的数据结构,这会降低性能。

2.很多繁重的操作仍旧在应用暂停期间进行,而不是像CMS几乎全部并发处理。个人认为随着堆的体积越来越大,这会成为G1大规模应用阻碍。

3.大对象(和G1区域大小相比)将成为G1的一个问题点(碎片化的原因)。

 

下面是G1收集器的日志:

G1年轻代收集:

[GC pause (young), 0.00176242 secs]
   [Parallel Time:   1.6 ms]
      [GC Worker Start Time (ms):  15751.4  15751.4]
      [Update RS (ms):  0.1  0.3
       Avg:   0.2, Min:   0.1, Max:   0.3]
         [Processed Buffers : 2 1
          Sum: 3, Avg: 1, Min: 1, Max: 2]
      [Ext Root Scanning (ms):  1.0  0.9
       Avg:   0.9, Min:   0.9, Max:   1.0]
      [Mark Stack Scanning (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
      [Scan RS (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
      [Object Copy (ms):  0.3  0.3
       Avg:   0.3, Min:   0.3, Max:   0.3]
      [Termination (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
         [Termination Attempts : 1 1
          Sum: 2, Avg: 1, Min: 1, Max: 1]
      [GC Worker End Time (ms):  15752.9  15752.9]
      [Other:   0.1 ms]
   [Clear CT:   0.0 ms]
   [Other:   0.1 ms]
      [Choose CSet:   0.0 ms]
   [ 18M->12M(26M)]
 [Times: user=0.00 sys=0.02, real=0.00 secs]

G1部分收集:

[GC pause (partial), 0.01589707 secs]
   [Parallel Time:  15.6 ms]
      [GC Worker Start Time (ms):  15774.1  15774.2]
      [Update RS (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
         [Processed Buffers : 0 3
          Sum: 3, Avg: 1, Min: 0, Max: 3]
      [Ext Root Scanning (ms):  1.0  0.7
       Avg:   0.8, Min:   0.7, Max:   1.0]
      [Mark Stack Scanning (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
      [Scan RS (ms):  0.0  0.1
       Avg:   0.0, Min:   0.0, Max:   0.1]
      [Object Copy (ms):  14.3  14.5
       Avg:  14.4, Min:  14.3, Max:  14.5]
      [Termination (ms):  0.0  0.0
       Avg:   0.0, Min:   0.0, Max:   0.0]
         [Termination Attempts : 3 3
          Sum: 6, Avg: 3, Min: 3, Max: 3]
      [GC Worker End Time (ms):  15789.5  15789.5]
      [Other:   0 helpful hints.4 ms]
   [Clear CT:   0.0 ms]
   [Other:   0.2 ms]
      [Choose CSet:   0.0 ms]
   [ 13M->12M(26M)]
 [Times: user=0.03 sys=0.00, real=0.02 secs]

G1全量收集(增量模式失败):

32.940: [Full GC 772M->578M(900M), 1.9597901 secs]

[Times: user=2.29 sys=0.08, real=1.96 secs]

 

Train GC

Train GC很久之前就被HotSpot JVM移除了。但是很多过时的介绍GC的文章,所以你有时还能从某些地方看到这个模式被提及。但是,它已经被移除了。

 

持久空间(Permanent space

介绍一下这个区域防止你有疑问。持久空间是年老代的一部分,JVM用来存储内部数据结构(大部分和类的加载和JIT有关)。持久空间在每次年老代收集循环中没有必要被清空,所以有时你需要添加额外的参数来让JVM清除持久空间里面的无用数据。通常持久空间里的数据是永恒不变的,但是JVM卸载class的能力使其变得非常复杂。