1、简介
对于Java developer来说,了解JVM GC工作原理能够帮助我们开发出更优秀的应用,同时在处理JVM瓶颈时能够更加自由。在最近一年的应用开发中能体会到这些知识带来的好处,并且让我们的应用在较大规模的并发时能够良好的工作。
本文部分知识和图片来源于书籍《Java Performance》 - Charlie Hunt & Binu John 著,该书全面讲解了Java 应用的性能分析、优化点与JVM原理等知识,本文(以及稍候的一些文章)只包含 GC collector 的部分知识,另外也只是针对常用的HotSpot JVM,当然很多知识是相通的。
2、 JVM Overview
上图为JVM的概要构架,主要分为 VM Runtime、GC Collector以及 JIT Compiler,其中我们能通过配置干预的只有GC 和 JIT(通过-server 和 -client选择不同的 JIT Compiler,其中-server在启动完成后具有更好的性能,但是花费启动时间稍长些 -- 虽然我们没明显感觉)。
下图则是JVM中内存分区模型,即分为 Heap area, method area, VM stack, native method stack和PC register。
本文只关注GC collector所涉及的Heap area 和 Method area, 其他的可另外了解。
3、Garbage Collections
在GC 中,最 ”引人入胜“ 的是 "stop-the-world",它在目前所实现的GC算法中都将出现,它出现的时候JVM 所承载的应用程序将停止执行,也就是说我们的应用中的所有线程将停止响应,直到 "stop-the-world" 完成工作。
HotSpot VM把 Heap Area分为 young generation 和 old generation两个物理区域,也就是我们常翻译为:年轻代和年老代。它们有以下特征:
1. 年轻代:大部分对象在这里分配,通常来说会进行比较小但是比较频繁的回收,花费时间较短。
2. 年老代:内存相对较大,对象会相对保留比较长的时间,大对象也一般分配在此,占用空间上升缓慢并且回收频率低,"stop-the-world" 会在它回收时发生,花费较长的时间。
如下图中,对象在年轻代分配,经过一些时间后可能会被移到年老代。其中的permanent generation为持久区,主要存放元数据如class data structures, interned strings等信息。GC 重点在于 young gen 和 old gen的回收。
3.1 Young generation
Young generation 有更细的划分,分为 Eden 和 2个survivor space(即幸存区) 如下图:
1. The eden: 大部分新对象在这里分配,有些大对象直接在年老区分配,这个区域进行回收后很多情况下会变空。
2. The two survivor spaces:Eden 区满时会将仍然存活的对象复制至其中一个survivor区,当这个区又满时将复制至另外一个survivor区,如下图:
3.2 Garbage Collectors
HotSpot 目前有三种可用的回收器: Serial GC, Parallel GC 和 Concurrent Mark-Sweep(CMS GC),回收类型分为mirror GC 和 major GC(也叫做Full GC),mirror GC针对young generation,major GC针对 old generation。
注:查询应用使用的垃圾回收与内存分配情况使用:jmap -heap [PID],其中PID为进程ID,稍候的文章会可能会有示例。
3.2.1 Serial GC
当Java 应用启动时如果加入-client 参数则使用这个回收器(如果不指定会根据官方的判断方法(如CPU核数,内存大小等)来决定使用 -server还是 -client,详情请查询官方资料),在目前的硬件配置来说,指定-server是大部分场景所需。
该回收器的特点是单线程回收(在这个多核时代明显很少被选择),mirro GC和major GC都需要暂停应用(即stop-the-world)
,如下图:
3.2.2 Parallel GC
在parallel GC 中,minor 和 full GC 都是并行的,可使用多核并行回收,可指定使用多少线程,能很好改善应用吞吐量,一般的机器配置中,如果不指定所使用的GC collector,一般默认为 Parallel GC,它符合大多数应用场景。如下图:
3.2.3 CMS GC
该回收器的出现主要应对低响应延时的应用场景,即 stop-the-world 一旦发生,应用将停止响应直到它工作完成,这情况在很多WEB、电信或银行应用中尤为关键。虽然 major GC很少发生,但是一旦发生将耗时较长,特别是在Heap很大的时候(也就是我们为什么不能让Heap分配很大的原因,适当即可,可有效分散GC暂停的时间使得不会感到明显卡顿)。
CMS GC 旨在减少应用的响应时间(但会牺牲一些吞吐量),由于延长了GC的工作周期,所以有更大的heap区要求(在marking pause阶段仍然允许分配内存),它同时具有更复杂的算法,CMS GC在管理 young generation方面与 Serial/Parallel GC一样(通过参数指定,稍候的文章中会有详解),在管理 old generation 采用2个周期来回收以减少应用暂停时间,如下图:
大致动作描述分为:
1. Initial mark,标记在old generation之外的所有可直接抵达的对象(如静态对象、线程栈等)。
2. Concurrent marking phase,在这个阶段标记所有可抵达的(直接或间接)存活对象,在这个阶段应用是不需要停止的,标记线程和应用线程同时工作。
3. Concurrent pre-cleaning为了减轻Remark pause步骤的工作,如访问在making phase阶段被更改的引用,虽然Remark pause还是会做这工作,但是它能显著地减少Remark pause的持续时间。
4. Remark pause,由于应用工作时会不停地更新引用,所以最后还是需要暂停应用来彻底标记,这步骤重新检查前一步骤(Concurrent marking phase)到暂停这时间段内更新的引用。
5. Concurrent sweeping phase,这步骤已经知道了所有在old generation存活的对象,将重新整理内存和释放不可达对象(如下图b所示),释放后标识空闲空间,但他们是不连续的(Serial和parallel GC 则采用下图a的压缩方式),所以将导致在old generation分配内存代价比较昂贵。Marking pause阶段能保证标记在这阶段存活的对象,但不能保证所有不可达的对象能被清理,如果本次回收不能清理,则下次Full GC的时候将被回收。
最后谈内存碎片问题:由于缺少内存压缩将使得内存碎片化,CMS 提供压缩周期,这周期也是stop the world,和serial、parallel GC 相似。使用-XX:CMSFullGCsBeforeCompaction参数指定full GC 多少次后进行内存压缩,默认为0,一般可设置为1,稍候的文章中会有相关GC的参数详解。
3.2.4 The Garbage-First GC
G1 GC 未来将作为 CMS GC 的一种替代(在Java 6 update 20 or later和 Java 7中才具有), 它可指定GC导致的应用停止时间,有不同的内存分布,提高应用实时性,但该GC 未经过大量的生产环境应用验证,目前只作为实验性特征使用。
关于这几个GC行为对比如下图:
4. 总结JVM 调优主要在3点
JVM 调优主要在3点:
1. 选择合适的 GC collector.
2. 指定合适的 heap area大小.
3. 指定young generation的比例(或大小),它影响到 Full GC发生的频率以及应用暂停时间.