HotSpot 的垃圾收集器
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,收集器所处的区域,则表示它是属于新生代还是老年代收集器。
并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待。
并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集器程序运行于另一个CPU之上。
Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。是一个单线程收集器,只会使用一个CPU或者一条收集线程去完成垃圾收集工作,进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
尽管有以上让人不能接受的地方,但是Serial收集器还是有其优点的:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集可以获得较高的收集效率。
实际到目前为止,Serial收集器依然是JVM运行在Client模式下的默认新生代收集器。
Serial / Serial Old 收集器运行示意图
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样。
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器是使用 -XX:+UserConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UserParNewGC 选项来强制指定它。
在单CPU环境中,ParNew收集器不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数和CPU的数量相同,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
ParNew / Serial Old 收集器运行示意图
Parallel Scavenge 收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
吞吐量 = 程序运行时间 / (程序运行时间 + 垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。高吞吐量适合高效利用CPU时间,主要用于后台运算不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数来精确控制吞吐量:
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间,是一个大于0的毫秒数,停顿时间设置小了就要牺牲相应的吞吐量和新生代空间。
-XX:GCTimeRatio
设置吞吐量大小,是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,默认值为99,也就是允许最大1%的垃圾回收时间。
-XX:+UseAdaptiveSizePolicy
这是一个开关参数,当这个参数打开后,就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为GC自适应的调节策略。这也是其与PreNew收集器的一个重要区别,也是其无法与CMS收集器搭配使用的原因(CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验)。
Parallel Scavenge / Parallel Old 收集器运行示意图
Serial Old 收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。
CMS 收集器
CMS收集器(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适合交互式应用的需求。
CMS收集器时基于“标记-清除”算法实现,运作过程分为四个步骤:
初始标记:需要进行GC停顿,标记 GC Roots 能直接关联到的对象,速度很快。
并发标记:进行可达性分析的过程。
重新标记:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一点,但远比并发标记的时间短。
并发清除:进行清除工作。
CMS的优点很明显:并发收集、低停顿。由于进行垃圾收集的时间主要耗在并发标记与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的。
尽管如此,CMS收集器的缺点也是很明显的,主要有3个缺点:
1、CMS收集器对CPU资源非常敏感。
在并发(并发标记、并发清除)阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量 + 3) / 4。因此CMS垃圾收集器始终不会占用少于25%的CPU,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,虚拟机提供了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占式来模拟多任务机制,在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经不提倡用户使用。
2、CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“失败而导致另一次 Full GC 的产生。
由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数 -XX:CMSInitiatingOccupancyFraction 的值来提高触发百分比,以降低内存回收次数提高性能。JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX:CMSInitiatingOccupancyFraction 设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
3、CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。
空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个 -XX:UseCMSCompactAtFullCollection 开关参数(默认开启),用于在Full GC之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,解决了空间碎片问题,却使停顿时间变长。所以还提供了 -XX:CMSFullGCBeforeCompaction 参数设置执行多少次不压缩的 Full GC 之后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。
CMS收集器运行示意图
G1收集器
G1(Garbage First)收集器是一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器。之前一直处于实验阶段,直到jdk7u4之后,才正式作为商用的收集器。
与前几个收集器相比,G1收集器有以下特点:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间。
分代收集:G1不需要与其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。
空间整合:从整体来看是基于“标记-整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现的,但是都意味着G1运行期间不会产生内存碎片空间,分配大对象时不会因为没有连续空间而进行下一次GC。
可预测的停顿:降低停顿时间是G1和CMS共同关注点,但G1除了追求低停顿,还能建立可预测的停顿模型,可以明确地指定在一个长度为M的时间片内,消耗在垃圾收集的时间不超过N毫秒
G1之前的收集器的手机范围都是整个新生代或者整个老年代,G1收集器将Java堆划分成多个大小相等的独立区域(Region),虽然保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合(不需要连续)。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收可以获得的空间大小和回收所需要的时间的经验值),后台维护一个优先队列,根据每次允许的收集时间,优先回收价值最大的Region(Garbage-First 理念)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间可以获得尽可能高的收集效率。
G1收集器Region之间的对象引用以及新生代和老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描。G1中每个Region都有一个与之对应的 Remembered Set,虚拟机发现程序在对引用类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查引用的对象是否处于不同的Region之中(分代的例子中就检查是否老年代对象引用了新生代的对象),如果是则通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set 即可保证不对全堆扫描也不会有遗漏。
忽略 Remembered Set 的维护,G1的运行步骤可简单描述为一下四步:
初始标记:标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这个阶段需要停顿线程,但耗时很短。
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,此阶段时间较长可与用户程序并发执行。
最终标记:修正在并发标记期间因用户线程继续运行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,需要停顿线程,但是可并行执行。
筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
G1收集器运行示意图
JVM调优
常用参数
参数 | 描述 |
---|---|
-XX:+UseSerialGC |
Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1。例如 -XX:SurvivorRatio=4 表示 Eden:S0:S1 = 4:3:3 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。-XX:PretenureSizeThreshold=1024 (单位为字节,默认为0) |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代,默认值为15。此参数只有在Serial 串行GC时有效。 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
参数补充:
-Xms、-Xmx
-Xms 堆的最小值。例如:-Xms20M(最小值)。-Xmx 堆的最大值。例如:-Xmx20M(最大值) 。
-Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。
-Xmn
堆中年轻代的大小。-Xmn 决定了年轻代空间的大小,年轻代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制。老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
-XX:PermSize、-XX:MaxPermSize
-XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。
JDK8中用 MetaspaceSize 代替 PermSize,因此将 -XX:PermSize=10M -XX:MaxPermSize=10M 修改为 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
-Xss
设置栈的大小。例如:-Xss128K,表示设置栈的大小为128K。
-XX:MaxDirectMemorySize
设置本地直接内存大小:-XX:MaxDirectMemorySize(如果不指定,默认与-Xmx的值一样)。
-XX:+/-UseTLAB
设置虚拟机上是否使用TLAB
-XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。
代大小调优
JVM调优主要是针对内存管理方面的调优,包括控制各个代的大小,GC策略。由于GC开始垃圾回收时会挂起应用线程,严重影响了性能,调优的目是为了尽量降低GC所导致的应用线程暂停时间、 减少 Full GC 次数。
1、避免年轻代设置过小
当年轻代设置过小时,会产生两种比较明显的现象,一是 Minor GC 次数频繁,二是可能导致 Minor GC 对象直接进入老年代。当老年代内存不足时,会触发 Full GC。
2、避免年轻代设置过大
年轻代设置过大,会带来两个问题:一是老年代变小,可能导致 Full GC 频繁执行;二是 Minor GC 执行回收的时间大幅度增加。
3、避免Survivor区过大或过小
-XX:SurvivorRatio参数值设置过大,就意味着Eden区域变大,Minor GC 次数会降低,但两块 Survivor 区域变小,如果超过 Survivor 区域内存大小的对象在 Minor GC 后仍没被回收,则会直接进入老年代。
-XX:SurvivorRatio参数值设置过小,就意味着Eden区域变小,Minor GC 触发次数会增加,Survivor 区域变大,意味着可以存储更多在 Minor GC 后仍然存活的对象,避免其进入老年代。
4、合理设置对象在年轻代存活的周期
年轻代存活周期的值决定了年轻代对象在经过多少次 Minor GC 后进入老年代。因此这个值要根据自己的应用来调优,JVM参数上这个值对应的为 -XX:MaxTenuringThreshold,默认值为15次。