学习JVM-GC收集器

1. 前言

  在上一篇文章中,介绍了JVM中垃圾回收的原理和算法。介绍了经过引用计数和对象可达性分析的算法来筛选出已经没有使用的对象,而后介绍了垃圾收集器中使用的三种收集算法:标记-清除、标记-整理、标记-复制算法。html

  介绍完原理,在这篇文章中,咱们将介绍当前JVM中已经实现的垃圾收集器,以及与收集器主题相关的一些内容。算法

  首先,咱们将在上一篇文章中提到分代收集机制的基础上,介绍下现代商业JVM中广泛采用的分代回收策略。而后,按照内存分代划分的维度介绍下当前JVM中实现的收集器。最后,学习分析不一样收集器的GC日志,而后结合日志分析,学习下不一样状况下的对象分配策略。服务器

2. 分代收集策略

  咱们知道,当对象被建立的时候,就会给对象分配一块内存空间,而一旦对象的生命周期结束,咱们就须要回收这块内存空间。可是,在一个应用程序中,不一样的对象存在的时间,或者说每一个对象的生命周期都是不一样的。多线程

  有些对象生命周期很短,好比Web应用程序中的request对象,它的生命周期和请求是对应的,当请求完成之后,该request对象就结束了它的职责,须要被收集器回收。有些对象的生命周期很长,好比一些全局的对象,可能会伴随整个应用程序的生命周期而存在。并发

  在上图中,横轴表示对象的生命周期长短,竖轴表示对应生命周期下的对象数量。观察蓝色的区域,咱们能够看到大部分的对象的生命周期都很短,而生命周期长的对象,它们的数量占据了小部分。性能

  考虑到不一样生命周期的对象的分布状况,为了合理的处理不一样生命周期的对象回收问题。现代JVM的对不一样生命周期的对象进行分类,对堆内存区域进行逻辑划分。按照对象的存活时间长短,将内存分为:年轻代、老年代和永久代(在Java8中去掉了永久代,以元数据空间代替)。这里咱们主要关注年轻代和老年代的GC。学习

  JVM提供了两个参数来控制JVM堆的大小:-XX:InitialHeapSize(-Xms)-XX:MaxHeapSize(-Xmx)。JVM会根据应用程序使用内存的状况,动态扩展堆内存的大小,上图中的Virtual表示的区域,表示的就是能够扩展的内存空间。优化

  好比,咱们能够将JVM的堆内存设置为256M,最大512M的大小,那么能够这么设置:-Xms256m -Xmx512m。若是将Xms的值和Xmx的值设置为相同,那么JVM将不能动态扩展堆内存,它的初始堆内存和最大堆内存是相同的。spa

2.1 年轻代

  在年轻代中,又将内存细分为Eden区和2个Survivor区,正常状况下,对象都是在Eden区被分配的。因为在年轻代,GC算法采用的是“标记-复制”算法,因此划分出了两个Survivor区,用于在执行复制算法的时候交替存放存活的对象。线程

  在JVM运行的时候,在年轻代,只有Eden区和其中的一个Survivor区会被使用,而另一个Surviro区是闲置的。当在年轻代进行GC的时候,会将此次GC之后存活的对象移动到其中闲置的Survivor区中,而后清空Eden区和以前的Survivor区。这样,就能够保证每次都有一快空闲的内存用于复制。

  JVM参数NewSizeMaxNewSize分别能够控制年轻代的初始大小和最大的大小。经过设置这两个参数,能够手动控制JVM中年轻代的大小,好比-XX:NewSize=100m将年轻代的大小初始化为100m。除了经过固定值来控制年轻代的大小,还能够经过参数NewRatio来按比例控制年轻代的大小,NewRatio的值表示年轻代和老年代的比值,好比:-XX:NewRatio=6 就表示,年轻代:老年代 = 1:6,因此年轻代占据了堆内存的1/7,而老年代则占据了6/7。

  对于年轻代中的Eden区和Survivor区的大小分配,JVM提供了SurvivorRatio这个参数来控制两块区域的大小。和NewRatio同样,这个值也是用于控制Eden区和两个Survivor区的大小比例的,好比:-XX:SurvivorRatio = 8,那么表示Eden : 一个Survivor = 8 : 1,那么Eden区就占据了年轻代中的8 / 10,而两个Survivor区分别占据了 1 / 10。

  咱们通常把在年轻代中进行的GC称为Minor GC

2.2 老年代

  当对象在年轻代中经历了屡次Minor GC之后仍旧存活,那么当达到必定的年龄(经历过一次Minor GC,年龄加1)之后,仍旧存活的对象就会被移动到老年代中。在老年代中的对象,通常是那些存活时间相对比较长的对象。正常状况下,在老年代的GC不会像年轻代那么频繁,老年代的GC收集器,通常采用"标记-清除"算法或"标记-整理"算法来回收垃圾对象。

  老年代中的对象,除了经过年轻代提高上来的长生命周期的对象之外,在一些特殊的状况下,也会在老年代中直接分配对象。具体状况,在下面的对象分配策略一节,会具体讲述。

  老年代的GC咱们通常称为Major GC

3. GC收集器

  前面,咱们介绍了JVM中对堆内存进行了分代的划分。目的,就是为了能够按照不一样的对象特色,合理的利用不一样的垃圾收集算法来处理垃圾对象。接下来,咱们来看下针对不一样的内存区域和使用场景,JVM中已经实现的那些GC收集器,了解下不一样的收集器的特性和适用场景以及它们的优缺点。

3.1 收集器概览

  Oracle Hotspot JVM中实现了多种垃圾收集器,针对不一样的年龄代内存中的对象的生存周期和应用程序的特色,实现了多款垃圾收集器。

  单线程GC收集器包括Serial和SerialOld这两款收集器,分别用于年轻代和老年代的垃圾收集工做。后来,随着CPU多核的普及,为了更好了利用多核的优点,开发了ParNew收集器,这款收集器是Serial收集器的多线程版本。

  多线程收集器还包括Parallel Scavenge和ParallelOld收集器,这两款也分别用于年轻代和老年代的垃圾收集工做,不一样的是,它们是两款能够利用多核优点的多线程收集器。

  相对来讲更加复杂的还有CMS收集器。这款收集器,在运行的时候会分多个阶段进行垃圾收集,并且在一些阶段是能够和应用线程并行运行的,提升了这款收集器的收集效率。

  其中最早进的收集器,要数G1这款收集器了。这款收集器是当前最新发布的收集器,是一款面向服务端垃圾收集器。

  上面简单介绍了多款不一样的垃圾收集器,虽然它们的特性不一样,可是有些GC收集器能够组合使用来应对不一样的应用的业务场景。下图给出了不一样收集器以及它们之间是否兼容,互相兼容的收集器能够组合使用。

  接下来,咱们来分别介绍下上面提到的那些GC收集器以及它们各自的特色。

3.2 年轻代收集器

  年轻代收集器包括Serial收集器、ParNew收集器以及Parallel Scavenge收集器。

Serial收集器

  Serial收集器是一款年轻代的垃圾收集器,使用标记-复制垃圾收集算法。它是一款发展历史最悠久的垃圾收集器。Serial收集器只能使用一条线程进行垃圾收集工做,而且在进行垃圾收集的时候,全部的工做线程都须要中止工做,等待垃圾收集线程完成之后,其余线程才能够继续工做。工做过程能够简单的用下图来表示:  

   从图中能够看到,Serial收集器工做的时候,其余用户线程都中止下来,等到GC过程结束之后,它们才继续执行。并且处理GC过程的只有一条线程在执行。因为Serial收集器的这种工做机制,因此在进行垃圾收集过程当中,会出现STW(Stop The World)的状况,应用程序会出现停顿的情况。若是垃圾收集的时间很长,那么停顿时间也会很长,这样会致使系统响应变的迟钝,影响系统的时候。

  虽然这款年迈的垃圾收集器只能使用单核CPU,可是正是因为它不能利用多核,在一些场景下,减小了不少线程的上下文切换的开销,能够在进行垃圾收集过程当中专心处理GC过程,而不会被打断,因此若是GC过程很短暂,那么这款收集器仍是很是简单高效的。

  因为Serial收集器只能使用单核CPU,在现代处理器基本都是多核多线程的状况下,为了充分利用多核的优点,出现了多线程版本的垃圾收集器,好比下面将要说到的ParNew收集器。

ParNew收集器

  ParNew垃圾收集器是Serial收集器的多线程版本,使用标记-复制垃圾收集算法。为了利用CPU多核多线程的优点,ParNew收集器能够运行多个收集线程来进行垃圾收集工做。这样能够提升垃圾收集过程的效率。

  和上面的Serial收集器比较,能够明显看到,在垃圾收集过程当中,GC线程是多线程执行的,而在Serial收集器中,只有一个GC线程在处理垃圾收集过程。ParNew收集器在不少时候都是做为服务端的年轻代收集器的选择,除了它具备比Serial收集器更好的性能外,还有一个缘由是,多线程版本的年轻代收集器中,只有它能够和CMS这款优秀的老年代收集器一块儿搭配搭配使用。

  做为一款多线程收集器,当它运行在单CPU的机器上的时候,因为不能利用多核的优点,在线程收集过程当中可能会出现频繁上下文切换,致使额外的开销,因此在单CPU的机器上,ParNew收集器的性能不必定好于Serial这款单线程收集器。若是机器是多CPU的,那么ParNew仍是能够很好的提升GC收集的效率的。

  ParNew收集器默认开启的垃圾收集线程数是和当前机器的CPU数量相同的,为了控制GC收集线程的数量,能够经过参数-XX:ParallelGCThreads来控制垃圾收集线程的数量。

Parallel Scavenge收集器

  Parallel Scavenge收集器是是一款年轻代的收集器,它使用标记-复制垃圾收集算法。和ParNew同样,它也会一款多线程的垃圾收集器,可是它又和ParNew有很大的不一样点。

  Parallel Scavenge收集器和其余收集器的关注点不一样。其余收集器,好比ParNew和CMS这些收集器,它们主要关注的是如何缩短垃圾收集的时间。而Parallel Scavenge收集器关注的是如何控制系统运行的吞吐量。这里说的吞吐量,指的是CPU用于运行应用程序的时间和CPU总时间的占比,吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)。若是虚拟机运行的总的CPU时间是100分钟,而用于执行垃圾收集的时间为1分钟,那么吞吐量就是99%。

  直观上,好像以缩短垃圾收集的停顿时间为目的和以控制吞吐量为目的差很少,可是适用的场景却不一样。对于那些桌面应用程序,为了获得良好的用户体验,在交互过程当中,须要获得快速的响应,因此系统的停顿时间要尽量的快以免影响到系统的响应速度,只要保证每次停顿的时间很短暂,假设每次停顿时间为10ms,那么即便发生不少次的垃圾收集过程,假设1000次,也不会影响到系统的响应速度,不会影响到用户的体验。对于一些后台计算任务,它不须要和用户进行交互,因此短暂的停顿时间对它而言并不须要,对于计算任务而言,更好的利用CPU时间,提升计算效率才是须要的,因此假设每次停顿时间相对很长,有100ms,而因为花费了很长的时间进行垃圾收集,那么垃圾收集的次数就会降下来,假设只有5次,那么显然,使用以吞吐量为目的的垃圾收集器,能够更加有效的利用CPU来完成计算任务。因此,在用户界面程序中,使用低延迟的垃圾收集器会有很好的效果,而对于后台计算任务的系统,高吞吐量的收集器才是首选。

  Parallel Scavenge收集器提供了两个参数用于控制吞吐量。-XX:MaxGCPauseMillis用于控制最大垃圾收集停顿时间,-XX:GCTimeRatio用于直接控制吞吐量的大小。MaxGCPauseMillis参数的值容许是一个大于0的整数,表示毫秒数,收集器会尽量的保证每次垃圾收集耗费的时间不超过这个设定值。可是若是这个这个值设定的太小,那么Parallel Scavenge收集器为了保证每次垃圾收集的时间不超过这个限定值,会致使垃圾收集的次数增长和增长年轻代的空间大小,垃圾收集的吞吐量也会随之降低。GCTimeRatio这个参数的值应该是一个0-100之间的整数,表示应用程序运行时间和垃圾收集时间的比值。若是把值设置为19,即系统运行时间 : GC收集时间 = 19 : 1,那么GC收集时间就占用了总时间的5%(1 / (19 + 1) = 5%),该参数的默认值为99,即最大容许1%(1 / (1 + 99) = 1%)的垃圾收集时间。

  Parallel Scavenge收集器还有一个参数:-XX:UseAdaptiveSizePolicy。这是一个开关参数,当开启这个参数之后,就不须要手动指定新生代的内存大小(-Xmn)、Eden区和Survivor区的比值(-XX:SurvivorRatio)以及晋升到老年代的对象的大小(-XX:PretenureSizeThreshold)等参数了,虚拟机会根据当前系统的运行状况动态调整合适的设置值来达到合适的停顿时间和合适的吞吐量,这种方式称为GC自适应调节策略。

  Parallel Scavenge收集器也是一款多线程收集器,可是因为目的是为了控制系统的吞吐量,因此这款收集器也被称为吞吐量优先收集器。

3.3 老年代收集器

  老年代收集包括:Serial Old收集器、Parallel Old收集器以及CMS收集器。

Serial Old收集器

  Serial Old收集器是Serial收集器的老年代版本,它也是一款使用"标记-整理"算法的单线程的垃圾收集器。这款收集器主要用于客户端应用程序中做为老年代的垃圾收集器,也能够做为服务端应用程序的垃圾收集器,当它用于服务端应用系统中的时候,主要是在JDK1.5版本以前和Parallel Scavenge年轻代收集器配合使用,或者做为CMS收集器的后备收集器。

Parallel Old收集器

  Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"标记-整理"算法。这个收集器是在JDK1.6版本中出现的,因此在JDK1.6以前,新生代的Parallel Scavenge只能和Serial Old这款单线程的老年代收集器配合使用。Parallel Old垃圾收集器和Parallel Scavenge收集器同样,也是一款关注吞吐量的垃圾收集器,和Parallel Scavenge收集器一块儿配合,能够实现对Java堆内存的吞吐量优先的垃圾收集策略。

  Parallel Old垃圾收集器的工做原理和Parallel Scavenge收集器相似。

  

CMS收集器

  CMS收集器是目前老年代收集器中比较优秀的垃圾收集器。CMS是Concurrent Mark Sweep,从名字能够看出,这是一款使用"标记-清除"算法的并发收集器。CMS
垃圾收集器是一款以获取最短停顿时间为目标的收集器。因为现代互联网中的应用,比较重视服务的响应速度和系统的停顿时间,因此CMS收集器很是适合在这种场景下使用。

  CMS收集器的运行过程相对上面提到的几款收集器要复杂一些。  

  从图中能够看出,CMS收集器的工做过程能够分为4个阶段:

  • 初始标记(CMS initial mark)阶段
  • 并发标记(CMS concurrent mark)阶段
  • 从新标记(CMS remark)阶段
  • 并发清除(CMS concurrent sweep)阶段

  从图中能够看出,在这4个阶段中,初始标记和从新标记这两个阶段都是只有GC线程在运行,用户线程会被中止,因此这两个阶段会发送STW(Stop The World)。初始标记阶段的工做是标记GC Roots能够直接关联到的对象,速度很快。并发标记阶段,会从GC Roots 出发,标记处全部可达的对象,这个过程可能会花费相对比较长的时间,可是因为在这个阶段,GC线程和用户线程是能够一块儿运行的,因此即便标记过程比较耗时,也不会影响到系统的运行。从新标记阶段,是对并发标记期间因用户程序运行而致使标记变更的那部分记录进行修正,从新标记阶段耗时通常比初始标记稍长,可是远小于并发标记阶段。最终,会进行并发清理阶段,和并发标记阶段相似,并发清理阶段不会中止系统的运行,因此即便相对耗时,也不会对系统运行产生大的影响。

  因为并发标记和并发清理阶段是和应用系统一块儿执行的,而初始标记和从新标记相对来讲耗时很短,因此能够认为CMS收集器在运行过程当中,是和应用程序是并发执行的。因为CMS收集器是一款并发收集和低停顿的垃圾收集器,因此CMS收集器也被称为并发低停顿收集器。

  虽然CMS收集器能够是实现低延迟并发收集,可是也存在一些不足。

  首先,CMS收集器对CPU资源很是敏感。对于并发实现的收集器而言,虽然能够利用多核优点提升垃圾收集的效率,可是因为收集器在运行过程当中会占用一部分的线程,这些线程会占用CPU资源,因此会影响到应用系统的运行,会致使系统总的吞吐量下降。CMS默认开始的回收线程数是(Ncpu + 3) / 4,其中Ncpu是机器的CPU数。因此,当机器的CPU数量为4个以上的时候,垃圾回收线程将占用很多于%25的CPU资源,而且随着CPU数量的增长,垃圾回收线程占用的CPU资源会减小。可是,当CPU资源少于4个的时候,垃圾回收线程占用的CPU资源的比例会增大,会影响到系统的运行,假设有2个CPU的状况下,垃圾回收线程将会占据超过50%的CPU资源。因此,在选用CMS收集器的时候,须要考虑,当前的应用系统,是否对CPU资源敏感。

  其次,CMS收集器在处理垃圾收集的过程当中,可能会产生浮动垃圾,因为它没法处理浮动垃圾,因此可能会出现Concurrent Mode Failure问题而致使触发一次Full GC。所谓的浮动垃圾,是因为CMS收集器的并发清理阶段,清理线程是和用户线程一块儿运行,若是在清理过程当中,用户线程产生了垃圾对象,因为过了标记阶段,因此这些垃圾对象就成为了浮动垃圾,CMS没法在当前垃圾收集过程当中集中处理这些垃圾对象。因为这个缘由,CMS收集器不能像其余收集器那样等到彻底填满了老年代之后才进行垃圾收集,须要预留一部分空间来保证当出现浮动垃圾的时候能够有空间存放这些垃圾对象。在JDK 1.5中,默认当老年代使用了68%的时候会激活垃圾收集,这是一个保守的设置,若是在应用中老年代增加不是很快,能够经过参数"-XX:CMSInitiatingOccupancyFraction"控制触发的百分比,以便下降内存回收次数来提供性能。在JDK 1.6中,CMS收集器的激活阀值变成了92%。若是在CMS运行期间没有足够的内存来存放浮动垃圾,那么就会致使"Concurrent Mode Failure"失败,这个时候,虚拟机将启动后备预案,临时启动Serial Old收集器来对老年代从新进行垃圾收集,这样会致使垃圾收集的时间边长,特别是当老年代内存很大的时候。因此对参数"-XX:CMSInitiatingOccupancyFraction"的设置,太高,会致使发生Concurrent Mode Failure,太低,则浪费内存空间。

  CMS的最后一个问题,就是它在进行垃圾收集时使用的"标记-清除"算法。上一篇文章介绍垃圾回收原理的时候,咱们讲到"标记-清除"算法,在进行垃圾清理之后,会出现不少内存碎片,过多的内存碎片会影响大对象的分配,会致使即便老年代内存还有不少空闲,可是因为过多的内存碎片,不得不提早触发垃圾回收。为了解决这个问题,CMS收集器提供了一个"-XX:+UseCMSCompactAtFullCollection"参数,用于CMS收集器在必要的时候对内存碎片进行压缩整理。因为内存碎片整理过程不是并发的,因此会致使停顿时间变长。"-XX:+UseCMSCompactAtFullCollection"参数默认是开启的。虚拟机还提供了一个"-XX:CMSFullGCsBeforeCompaction"参数,来控制进行过多少次不压缩的Full GC之后,进行一次带压缩的Full GC,默认值是0,表示每次在进行Full GC前都进行碎片整理。

  虽然CMS收集器存在上面提到的这些问题,可是毫无疑问,CMS当前仍然是很是优秀的垃圾收集器。

4. GC日志分析

  垃圾收集器在进行垃圾收集的过程当中,能够输出日志,咱们经过日志,能够看到当前垃圾收集器的运行状况。经过gc日志,咱们能够观察垃圾收集器的行为,以及当前应用程序的GC状况和内存使用状况。学会查看和分析垃圾收集日志,一方面能够帮助咱们学习垃圾收集器;另外一方面,在必要的时候,能够帮助咱们定位问题,解决问题,对JVM进行优化。

  默认,JVM不会打印出GC日志信息,能够经过参数-XX:+PrintGC或-verbose:gc来设置JVM输出gc日志到终端中。

  JVM参数:-XX:+PrintGC -XX:+UseSerialGC -Xms10m -Xmx10m

[GC (Allocation Failure)  1922K->1394K(9920K), 0.0021245 secs] [Full GC (Allocation Failure) 7585K->7538K(9920K), 0.0023668 secs]

  当设置了"-XX:+PrintGC"或者"-verbose:gc"之后就会输出相似输出上面的GC日志。这是最简单的GC日志,包含了垃圾收集过程当中的信息。其中红色部分的"GC"和"Full GC"表示此次GC的类型,而绿色部分的"Allocation Failure"表示表示发生此次GC的缘由,从上面的日志能够看出,是因为内存分配失败致使的GC。后面的黄色部分"1922K->1394K(9920K)"表示此次GC致使JVM中堆内存的使用量从1922K下降到了1394K,其中括号中表示当前整个JVM堆的大小。最后蓝色部分的"0.0021245 secs"表示此次GC持续的时间。

  上面输出的是简单格式的GC日志,虽然提供了一些信息,可是经过这些信息,咱们无法知道此次GC发生的时候,此次GC是发生在老年代仍是在年轻代,是否有对象从年轻代被移动到了老年代等信息,因此咱们但愿能够看到更加详尽的信息。这个时候,咱们须要设置-XX:+PrintGCDetails参数来输出更加详细的GC日志,下面咱们结合不一样的收集器组合,来分析下它们的输出日志。

Serial GC + Serial Old

  Serial GC和Serial Old收集器是比较早的单线程收集器,工做原理咱们在上面已经介绍过了。这里,咱们来看下使用这两款收集器进行垃圾收集的时候,输出的日志格式是怎么样的。首先咱们须要设置JVM参数:

  JVM参数:-XX:+PrintGC -XX:+PrintGCDetails -XX:+UseSerialGC -Xms10m -Xmx10m

[GC (Allocation Failure) 
[DefNew: 1922K->319K(3072K), 0.0027356 secs] 1922K->1394K(9920K), 0.0027698 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[Tenured: 6514K->6484K(6848K), 0.0025899 secs] 8562K->8532K(9920K), [Metaspace: 2984K->2984K(1056768K)], 0.0026153 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]

   能够发现,经过设置了"-XX:+PrintGCDetails"之后,输出的GC日志信息多了不少。咱们先来看第一条,红色部分"GC"表示此次发生的是Minor GC,绿色部分"Allocation Failure"表示致使此次GC的缘由是内存分配失败。接下来,黄色部分的内容,则和前面的日志有些区别了,这里输出的内容相对比较详细。"DefNew: 1922K->319K(3072K), 0.0027356 secs] 1922K->1394K(9920K), 0.0027698 secs",其中DefNew表示此次GC发生在年轻代(不一样的收集器,日志的格式不必定相同),接下来"1922K->319K"表示此次GC致使年轻代使用的内存从1922K降到319K,括号中的"3072K"表示年轻代中的堆内存大小为3072K。"0.0027356 secs"表示此次年轻代GC耗时0.0027356s。后面的"1922K->1393K"表示总的堆内存(年轻代 + 老年代)的使用状况的变化,从1922K下降到1394K, 括号中的"9920K"表示总的堆内存的大小。最后的"0.0027698 secs"表示此次GC总的消耗的时间。最后是此次GC消耗的时间的统计,其中user表示用户态CPU执行的时间,sys表示内核态CPU执行的时间,这两个时间不包括被挂起消耗的时间,而real表示的是实际的时间,能够认为是墙上时钟走过的时间。

  下面的这条日志,"Full GC"表示此次GC是一次Major GC,后面的缘由和上面同样。咱们来看下黄色部分,"Tenured"表示此次GC发生在老年代,其中"6524K->6484K"表示老年代内存从6524K下降到6484K。后面的时间"0.0025899 secs"表示此次老年代GC耗时0.0025899s。接下来的"8562K -> 8532K"和上面提到的同样,表示整个堆内存的变化。最后的时间表示此次GC的总耗时为"0.0026153s"。

Parallel Scanvage + Parallel Old

  不一样的垃圾收集器,输出的日志信息也不是彻底相同的,上面咱们看到的日志,是使用Serial GC和Serial Old收集器输出的gc日志,而下面的日志信息,则是使用Parallel Scavenge收集器和Parallel Old收集器输出的日志。

  JVM参数:-XX:+PrintGC -XX:+UseParallelOldGC -XX:+PrintGCDetails -Xms10m -Xmx10m

[GC (Allocation Failure) --
[PSYoungGen: 1391K->1391K(2560K)] 7537K->7537K(9728K), 0.0007436 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[PSYoungGen: 1391K->1374K(2560K)]
[ParOldGen: 6145K->6145K(7168K)] 7537K->7520K(9728K), [Metaspace: 2984K->2984K(1056768K)], 0.0037697 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

  能够看到,使用Parallel Scavenge 和 Parallel Old收集器输出的日志,会有一些不一样,不过日志内容大致上差很少。最后,咱们来看下CMS垃圾收集器的日志是怎么样的,相对上面几款收集器,CMS相对更加复杂,从它输出的日志也能够看出来。

ParNew + Concurrent Mark Sweep(CMS)

  下面,咱们来看下ParNew配合CMS收集器在进行垃圾收集的时候,输出的GC 日志信息。

  JVM参数:-XX:+PrintGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms10m -Xmx10m

[GC (Allocation Failure) [ParNew: 2418K->0K(3072K), 0.0032236 secs] 3508K->3455K(9920K), 0.0032520 secs] 
[Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 3455K(6848K)] 4479K(9920K), 0.0005566 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-mark-start] [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-preclean-start] [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (CMS Final Remark) [YG occupancy: 1024 K (3072 K)]
[Rescan (parallel) , 0.0001118 secs][weak refs processing, 0.0000191 secs][class unloading, 0.0002858 secs]
[scrub symbol table, 0.0003506 secs][scrub string table, 0.0001305 secs]
[1 CMS-remark: 3455K(6848K)] 4479K(9920K), 0.0009500 secs]
[Times: user=0.00 sys=0.00, real=0.01 secs] [CMS-concurrent-sweep-start] [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-reset-start] [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  经过第一条日志,能够看出咱们使用"-XX:+UseConcMarkSweepGC"指定CMS垃圾收集器的时候,使用的是ParNew + CMS收集器组合。下面输出的一堆日志,就是CMS收集器在进行垃圾收集过程当中输出的信息。能够明显的看到,CMS在进行垃圾收集的过程当中,经历了4个阶段,在日志中我用4中颜色标记出来了。须要注意的是黄色部分,这是CMS的从新标记的阶段,在上面咱们介绍CMS收集器的时候说过,在这个阶段,是会出现Stop The World的,因此若是这个阶段消耗的时间比较长,则会影响应用的响应时间。

其余日志参数

  有时候,咱们须要在GC日志中输出时间值,这样咱们就能够知道此次GC发生的具体时间点。咱们能够经过JVM参数"-XX:+PrintGCTimeStamps" 和"-XX:+PrintGCDateStamps"来设置日志输出的时间。使用"-XX:+PrintGCTimeStamps"参数,能够在输出的日志前加上产生日志的时间戳:

7.327: [GC (Allocation Failure) 7.327: [DefNew: 2095K->2095K(3072K), 0.0000209 secs]

  能够看到,输出的日志中,在头部包含了一个时间戳,表示从JVM启动以来通过的秒数。而"-XX:+PrintGCDateStamps"则表示输出日志时的当前时间,相对来讲更加直观:

2017-02-24T00:14:38.611-0800: [GC (Allocation Failure) 
2017-02-24T00:14:38.611-0800: [DefNew: 1922K->319K(3072K), 0.0025676 secs] 1922K->1394K(9920K), 0.0026134 secs]

  除了将日志输出到控制台,咱们还能够将日志输出到日志文件中,这样就能够经过分析日志文件来分析系统的GC状况了,通常在服务器运行过程当中,咱们都会将GC日志输出到指定的文件中,供须要的时候分析。能够经过JVM参数"-Xloggc:<file>"来指定日志输出的目录。  

5. 总结

  在这篇文章中,咱们讨论了现代Java虚拟机中已经实现了的垃圾收集器。从分代收集策略出发,结合上一篇文章中介绍的垃圾收集原理,介绍了多款垃圾收集器的是实现。最后,咱们分析了垃圾收集器的GC日志,学习如何经过垃圾收集的日志,分析当前系统的垃圾收集的情况。文章到这里差很少就介绍, 但愿这篇文章能够帮助到你们!