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的能力使其变得非常复杂。