什么是垃圾收集

从字面意思来说,顾名思义垃圾收集就是处理垃圾。实际上,它的做法恰恰相反,垃圾收集正在追踪所有仍在使用的对象,将其余的对象标记为垃圾。下面我们深入探究 Java 虚拟机实现“垃圾收集”的自动内存回收过程。

内存管理

内存管理主要分为手动内存管理和自动内存管理。

手动内存管理

在C语言中通过malloc申请内存,通过free释放内存;C++通过new申请对象使用的内存,通过delete释放内存。对于C和C++这种手动管理内存的方式,很容易忘记释放内存,从而造成内存泄漏。因此,更好的方法是自动回收未使用的内存,这种自动化称为垃圾收集(即GC)。

自动内存管理

自动内存管理让开发者不再需要考虑自己清理内存,极大地提高了开发效率。但是这种自动内存管理,也就是说自动释放内存,它该如何定位到垃圾呢?

引用计数

Reference Count称为引用计数,为了标记存活对象,引用计数会在每个对象的头上引入一个叫“计数器”的东西,用来记录有多少对象引用了它。

A a = new A();
B b = new B();
a.ref = b;

对象A的实例在堆中就是一块内存,而a引用了它,所以它的引用就是1,对象B的实例在堆中也是一块内存,首先b引用了它,然后a又引用它一次,所以引用计数就是2。所以通过引用计数就能定位垃圾。但是引用计数有个非常致命的缺点,就是它不能找到循环引用的垃圾,这种情况,这些循环引用的对象是不能被回收的,其实它们是一团垃圾。

根可达算法

Root Searching称为根可达算法,java垃圾收集使用的就是根可达算法,即从GC根开始,遍历所有可到达的对象,所有找不到的对象都视为垃圾。

不太好的事情就是应用程序线程需要停止才能进行,因为如果引用一直发生变化,就无法真正计算引用,当应用程序线程真正停止,垃圾收集线程开始进行GC活动,这种情况称为Stop the World。

常见的垃圾回收算法

标记清除

在使用上最简单的垃圾回收算法,分为两个阶段,一个是标记阶段,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,对死亡的对象进行清除。

优点

  • 实现简单
  • 不需要移动对象

缺点

  • 产生内存碎片,清理后的内存可能不连续,容易出现内存很多,但是分配大对象时找不到合适的位置
  • 效率低,每次都需要遍历整个堆

复制

将内存一分为二,每次只使用一半,将标记存活的对象复制到另一片内存,然后一下子清空当前内存。

优点

  • 效率高,主要是标记阶段和复制是可以同时发生的
  • 不会产生内存碎片

缺点

  • 内存利用率低

标记整理

将所有标记存活的对象移动到内存区域的一端,然后再对其他内存进行清理。

优点

  • 不会产生内存碎片

缺点

  • 效率低,需要重新更新存活对象的引用地址

内存池

Eden

当对象在创建时会被分配到该区域,由于通常有多个线程创建大量对象,因此Eden被进一步划分为驻留在Eden空间中的一个或多个 Thread Local Allocation Buffer(TLAB),即线程本地分配缓冲区。由于线程之间会存在争抢同一块内存区域,会进行昂贵的线程同步开销,因此JVM允许在相应的TLAB中分配一个线程内的大多数对象。

由于每个线程分配的TLAB空间有限,当对象无法分配在里面时,JVM会将该对象分配在Eden的其他空间。如果那里也没有足够的空间,就会触发Young Generation来进行垃圾回收以释放更多空间,如果垃圾回收也没有在Eden中产生更多的空间,那么对象会被分配到老年代。

收集Eden区时,GC会从根遍历所有可到达的对象并将它们标记为活动对象,标记阶段完成后,将Eden中所有活动对象(以及Survivor空间之一未满足年龄的存活对象)复制到Survivor1或者Survivor2。这时清空Eden区和Survivor空间之一,可以重用以分配更多的对象,这种方法称为“复制“算法,简单来说就是标记存活对象,然后复制(而不是移动)到Survivor Spaces。

Survivor Spaces

Eden区旁边驻留着2个Survivor区称为S1和S2,有的地方也叫from和to,注意的是,两个Survivor空间中的一个必定为空。

年轻代被收集时,空的Survivor区将开始出现存活对象,因为整个年轻代(Eden区和非空的Survivor区)的所有存活对象都被复制到空的Survivor区。

当两个Survivor空间之间复制对象的过程重复多次,直到某些对象达到足够的”年龄“,这些对象不在被复制到空的Survivor区,而是进入年老代,在年老代,他们将驻留直到无法访问为止。

GC会跟踪特定对象存活下来的集合数量,在每一代对象GC结束后,那些仍然存活的对象的年龄会增加,当年龄超过任期阈值时,对象就会被提升到老年代。实际的任期阈值由JVM动态调整,但也可以通过指定 -XX:+MaxTenuringThreshold 为其设置上限,当设置 -XX:+MaxTenuringThreshold=0 时,会导致对象立即进入老年代,不会在Survivor之间进行复制。默认情况下,JVM设置此阈值为15个GC周期,这也是HotSpot中的最大值。

如果Survivor空间大小不足以容纳年轻代中所有活动对象,那么对象提升年老代也可能会过早发生。

Old Generation

相比于年轻代,老年代通常空间要大得多,并且老年代GC发生频率低于年轻代。此外,由于大多数对象都存活在老年代,因此不会使用”复制“算法,相反对象会四处移动以最大程度地减少碎片,原则上,老年代垃圾回收步骤如下:

  • 通过GC根访问所有对象旁边地标记位来标记可达的对象
  • 清除所有无法访问的对象
  • 通过将活动对象复制到老年代的边界来整理空间

从上面描述可以看出,老年代中的GC必须使用”标记整理“算法以避免过度的内存碎片化。

PermGen

在 Java8 之前,存在一个称为”永久代“的特殊空间(方法区的一种实现),主要用来存储class相关信息,包括class对象的Method、Field等。永久代使用的是JVM内存,实际上它给java开发人员带来很多麻烦,因为很难预测这些需要多少空间,所以方法区也会出现OOM(java.lang.OutOfMemoryError: Permgen space),对于这种情况只能简单的增加Permgen大小。

java -XX:MaxPermSize=256m com.mycompany.MyApplication

Metaspace

由于预测对元数据空间是一件非常困难的,因此 Java8 中删除了永久代,取而代之的是元空间(方法区的一种实现),两者最大的区别是元空间使用本地内存,而永久代使用的是JVM内存,这样的话默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大,这就解决了java.lang.OutOfMemoryError: Permgen space的问题,不过也不可能任其无限大,JVM默认在运行时会根据需要动态的设置其大小。

如果仍然希望限制Metaspace大小,可以设置:

java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication

Minor GC vs Major GC vs Full GC

清理堆内存的垃圾回收事件通常称为Minor、Major和Full GC。

Minor GC

从Young区收集垃圾的事件称为Minor GC,但是在处理Minor GC事件时,应该注意以下几点:

  • Minor GC总是发生在JVM无法为新对象分配空间时触发,例如Eden区没有足够空间,所以正常来说分配新对象频率越高,Minor GC频率也越高。
  • 在Minor GC事件期间,老年代会被忽略,从老年代到年轻代的引用会被认为是GC根,在标记阶段,从年轻代到老年代的引用会被简单地忽略。
  • Minor GC事件期间,会触发 stop-the-world 暂停,即暂停应用程序线程。对于大多数应用程序,如果Eden中大多数对象都被视为垃圾并且永远不会复制到Surivor/Old空间,那么暂停在延迟方面可以忽略不计。如果情况相反,大多数新生对象都会被复制地话,那么Minor GC暂停会花费更多时间。

Major GC 与 Full GC

应该注意的是,Major GC 和 Full GC还是存在区别的:

  • Major GC清理Old空间
  • Full GC清理整个Heap,包括Young和Old空间

但是许多Major GC是由Minor GC触发的,因此许多情况下不能将Major GC和Full GC分开。另一方面,对于像G1这种现代垃圾收集器,回收垃圾只是执行部分垃圾的清理,因此”清理“两字也并不严谨。

常见的垃圾回收器

在上面已经介绍了GC算法的核心概念,那么来看看在JVM中的具体实现。首先先认识到一个重要方面是,对于分代模型,需要两种不同的GC算法:一种适用于清理年轻代,另一种适用于清理年老代。以下适用于Java8,对于较久的Java版本,可用的组合略有不同:

年轻代 老年代 JVM选项
Serial Serial Old -XX:+UseSerialGC
Serial CMS -XX:-UseParNewGC -XX:UseConcMarkSweepGC
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel Scavenge Serial Old -XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel New CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel New Serial Old -XX:+UseParNewGC -XX:-UseParallelOldGC
G1 -XX:+UseG1GC

实际上在真正的使用上,并没有这么多组合,最常用的组合:

  • 串行:Serial + Serial Old
  • 并行:Parallel Scavenge + Parallel Old
  • 年轻代并行新 Parallel New + 老年代并发标记和清除 CMS
  • G1 包含Young和Old集合

Serial GC

Serial收集器是最基本、最悠久的垃圾收集器,JDK1.3之前新生代唯一的选择。

这个垃圾收集器对年轻代使用”复制“算法,对老年代使用“标记整理”算法。顾名思义,这个垃圾收集器是单线程收集器,无法并行处理任务,并且在GC时,还会触发stop-the-world暂停,即停止所有应用程序线程。

因此,这个垃圾收集器无法适用于多核CPU。

为年轻代和老年代启动该垃圾收集器,可以通过下面参数指定:

java -XX:+UseSerialGC com.mypackages.MyExecutableClass

Serial GC仅推荐用于200MB大小的内存,以及具有单个CPU的环境。

下面来看下Serial GC的日志:

2015-05-26T14:45:37.987-0200: 151.126: [GC (Allocation Failure) 151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 1619346K->1273247K(2027264K), 0.0585007 secs] [Times: user=0.06 sys=0.00, real=0.06 secs]

2015-05-26T14:45:59.690-0200: 172.829: [GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 1832479K->755802K(2027264K), [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs] [Times: user=0.18 sys=0.00, real=0.18 secs]

上面日志中发生了两个垃圾回收事件,一个是清理年轻代,另一个是处理整个堆。

Minor GC

2015-05-26T14:45:37.987-0200 1: 151.126 2: [GC 3 (Allocation Failure 4) 151.126: [DefNew 5: 629119K->69888K 6(629120K) 7, 0.0584157 secs] 1619346K->1273247K 8(2027264K) 9, 0.0585007 secs 10] [Times: user=0.06 sys=0.00, real=0.06 secs] 11

  1. 2015-05-26T14:45:37.987-0200 - GC事件开始的时间

  2. 151.126 - GC事件开始时间,相对于JVM启动时间,以秒为单位

  3. GC - 用于区分Minor和Full GC的标志,这是一个Minor GC

  4. Allocation Failure - GC发生的原因

  5. DefNew - 使用的垃圾收集器名称,这个表示用于清理年轻代的单线程标记复制、stop-the-world 收集器

  6. 629119K->69888K - 在收集前后年轻代使用大小

  7. (629120K) - 年轻代的总大小

  8. 1619346K->1273247K - 收集前后整个堆的使用大小

  9. (2027264K) - 整个堆的大小

  10. 0.0585007 secs - GC事件的持续事件(单位为秒)

  11. Times: user=0.06 sys=0.00, real=0.06 secs - GC持续时间,按不同类别衡量:

    user - 垃圾收集器线程在此收集期间消耗的总CPU时间

    sys - 花费在OS调用或等待系统事件上的事件

    real - 应用程序停止的时间,由于串行垃圾收集器总是使用一个线程,因此real时间等于用户时间和系统时间之和

Full GC

2015-05-26T14:45:59.690-0200 1: 172.829 2: [GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs 3]172.829: [Tenured 4: 1203359K->755802K 5(1398144K) 6, 0.1855567 secs 7] 1832479K->755802K 8(2027264K) 9, [Metaspace: 6741K->6741K(1056768K) 10], 0.1856954 secs] [Times: user=0.18 sys=0.00, real=0.18 secs] 11

  1. 2015-05-26T14:45:59.690-0200 - GC事件开始的时间

  2. 172.829 - GC事件开始时间,相对于JVM启动时间,以秒为单位

  3. DefNew: 629120K->629120K(629120K), 0.0000372 secs - 与前面类似,由于分配失败,在此期间发生了年轻代的GC,年轻代总大小629120K,此次GC回收了0K,耗时0.0000372秒

  4. Tenured - 用于清理老年代的垃圾收集器名称,名称Tenured表示使用单线程、stop-the-world 收集器

  5. 1203359K->755802K - GC前后老年代使用量

  6. 1398144K - 老年代的总大小

  7. 0.1855567 secs - 清理老年代所花费的时间

  8. 1832479K->755802K - 在年轻代和老年代收集前后整个堆的使用量

  9. 2027264K - JVM总堆的大小

  10. Metaspace: 6741K->6741K(1056768K - 元空间集合的信息,这次事件没有在Metaspace中收集垃圾

  11. Times: user=0.18 sys=0.00, real=0.18 secs - GC持续时间,按不同类别衡量:

    user - 垃圾收集器线程在此收集期间消耗的总CPU时间

    sys - 花费在OS调用或等待系统事件上的事件

    real - 应用程序停止的时间,由于串行垃圾收集器总是使用一个线程,因此real时间等于用户时间和系统时间之和

Parallel GC

Parallel收集器在年轻代采用”复制“算法,在老年代使用”标记整理“算法。同样,Young和Old收集都会触发stop-the-world暂停,停止所有的应用程序线程,Parallel收集器会在GC时使用多个线程进行复制/标记整理,这大大减少了收集时间和stop-the-world时间。

该收集器使用的线程数可通过命令行参数 -XX:ParallelGCThreads 进行配置,默认值是机器的内核数。

Parallel GC通过下面任意一个进行开启:

java -XX:+UseParallelGC com.mypackages.MyExecutableClass
java -XX:+UseParallelOldGC com.mypackages.MyExecutableClass
java -XX:+UseParallelGC -XX:+UseParallelOldGC com.mypackages.MyExecutable

相对于Serial单线程收集,Parallel收集器可以提高吞吐量,适用于多核机器。在收集期间,所有内核都在并行进行清理垃圾,从而缩短应用程序线程暂停的时间;但是还是会有长时间暂停的影响,如果延迟是您的主要目标,可以考虑下面的垃圾收集器。

下面来看下Parallel GC的日志:

2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) [PSYoungGen: 2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K), 0.2406675 secs] [Times: user=1.77 sys=0.01, real=0.24 secs]

2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) [PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K), [Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs] [Times: user=4.49 sys=0.64, real=0.92 secs]

Minor GC

2015-05-26T14:27:40.915-0200 1: 116.115 2: [GC 3 (Allocation Failure 4) [PSYoungGen 5: 2694440K->1305132K 6(2796544K) 7] 9556775K->8438926K 8(11185152K) 9, 0.2406675 secs 10] [Times: user=1.77 sys=0.01, real=0.24 secs] 11

  1. 2015-05-26T14:27:40.915-0200 - GC事件开始时间

  2. 116.115 - GC事件开始时间,相对于JVM启动,以秒为单位

  3. GC - 用于区分Minor和Full GC,这是一个Minor GC

  4. Allocation Failure - GC事件发生的原因

  5. PSYoungGen - 使用的垃圾收集器名称,表示清理年轻代的并行标记复制、stop-the-world 收集器

  6. 2694440K->1305132K - GC前后年轻代的使用量

  7. 2796544K - 年轻代总大小

  8. 9556775K->8438926K - GC前后总堆的大小

  9. 11185152K - 总堆的大小

  10. 0.2406675 secs - GC事件持续事件(以秒为单位)

  11. Times: user=1.77 sys=0.01, real=0.24 secs - GC事件持续事件,按不同类别衡量:

    user - 垃圾收集器线程在此收集期间消耗的总CPU时间

    sys - 花费在OS调用或等待系统事件上的事件

    real - 应用程序停止的时间,对于Parallel GC,这个数字应该接近(user+sys)/垃圾收集器线程数

Full GC

2015-05-26T14:27:41.155-0200 1: 116.356 2: [Full GC 3 (Ergonomics 4) [PSYoungGen: 1305132K->0K(2796544K) 5] [ParOldGen 6: 7133794K->6597672K 7(8388608K) 8] 8438926K->6597672K 9(11185152K) 10, [Metaspace: 6745K->6745K(1056768K) 11], 0.9158801 secs 12] [Times: user=4.49 sys=0.64, real=0.92 secs] 13

  1. 2015-05-26T14:27:41.155-0200 - GC事件开始时间

  2. 116.356 - GC事件开始时间,相对于JVM启动,以秒为单位

  3. Full GC - 用于区分Minor和Full GC,这是一个Full GC

  4. Ergonomics - GC事件发生的原因,是由于开启了UseAdaptiveSizePolicy,jvm本身进行自适应调整引起的Full GC

  5. PSYoungGen: 1305132K->0K(2796544K) - 使用的垃圾收集器名称,表示清理年轻代的并行标记复制、stop-the-world 收集器,年轻代总大小2796544K,GC清理后从1305132K缩减到0K

  6. ParOldGen - 用于清理老年代的收集器,表示使用清理老年代的并行标记整理、stop-the-world 收集器

  7. 7133794K->6597672K - GC前后老年代的使用量

  8. 8388608K - 老年代总大小

  9. 8438926K->6597672K - GC前后总堆的大小

  10. 11185152K - 总堆的大小

  11. Metaspace: 6745K->6745K(1056768K) - 元空间集合的信息,这次事件没有在Metaspace中收集垃圾

  12. 0.9158801 secs - GC事件持续事件(以秒为单位)

  13. Times: user=4.49 sys=0.64, real=0.92 secs - GC事件持续事件,按不同类别衡量:

    user - 垃圾收集器线程在此收集期间消耗的总CPU时间

    sys - 花费在OS调用或等待系统事件上的事件

    real - 应用程序停止的时间,对于Parallel GC,这个数字应该接近(user+sys)/垃圾收集器线程数

Concurrent Mark and Sweep

这个垃圾收集器官方名称是”Mostly Concurrent Mark and Sweep Garbage Collector“。它在年轻代使用”复制”算法,并行进行垃圾回收,在老年代使用并发的“标记清除”算法。

CMS收集器目标是避免在老年代长时间停顿,它通过两种方式实现。首先,它不压缩老年代空间,而是通过空闲列表来管理回收空间。其次,它与应用程序线程同时完成标记和清理阶段的大部分工作。这意味着垃圾收集器不会显式停止应用程序线程来执行这些阶段,然后这也会导致垃圾回收线程和应用程序线程会竞争CPU时间。默认情况下,该收集器的线程数等于机器物理内核数的1/4。

可以通过在命令行指定以下参数启动此垃圾收集器:

java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass

如果您的目标是延迟,那么ParNew + CMS 这种垃圾收集器组合是个不错的选择。

下面来看看该组合垃圾收集器的日志:

2015-05-26T16:23:07.219-0200: 64.322: [GC (Allocation Failure) 64.322: [ParNew: 613404K->68068K(613440K), 0.1020465 secs] 10885349K->10880154K(12514816K), 0.1021309 secs] [Times: user=0.78 sys=0.01, real=0.11 secs]

2015-05-26T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Minor GC

2015-05-26T16:23:07.219-0200 1: 64.322 2: [GC 3 (Allocation Failure 4) 64.322: [ParNew 5: 613404K->68068K 6(613440K) 7, 0.1020465 secs 8] 10885349K->10880154K 9(12514816K) 10, 0.1021309 secs 11] [Times: user=0.78 sys=0.01, real=0.11 secs] 12

  1. 2015-05-26T16:23:07.219-0200 - GC事件开始时间

  2. 64.322 - GC事件开始时间,相对于JVM启动,以秒为单位

  3. GC - 用于区分Minor和Full GC,这是一个Minor GC

  4. Allocation Failure - GC事件发生的原因

  5. ParNew - 使用的垃圾收集器名称,表示清理年轻代的并行标记复制、stop-the-world 收集器

  6. 613404K->68068K - GC前后年轻代的使用量

  7. 613440K - 年轻代总大小

  8. 0.1020465 secs - 不带最终清理的持续时间

  9. 10885349K->10880154K - GC前后总堆的大小

  10. 12514816K - 总堆的大小

  11. 0.1021309 secs - GC事件持续事件(以秒为单位)。这包括和CMS收集器的通信开销、对老年代足够老的对象的提升以及在垃圾收集周期结束的一些最终清理

  12. Times: user=0.78 sys=0.01, real=0.11 secs - GC事件持续事件,按不同类别衡量:

    user - 垃圾收集器线程在此收集期间消耗的总CPU时间

    sys - 花费在OS调用或等待系统事件上的事件

    real - 应用程序停止的时间,对于Parallel GC,这个数字应该接近(user+sys)/垃圾收集器线程数

Full GC

CMS在老年代的垃圾收集分为不同阶段首先看这个Full GC日志:

2015-05-26T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
第一阶段:初始标记

该阶段会触发stop-the-world暂停。这个阶段的目标是标记老年代中的所有对象,这些对象要么是直接的GC根,要么是从年轻代中某个活动对象引用的。后者很重要,因为老年代是单独收集的。

2015-05-26T16:23:07.321-0200: 64.425 1: [GC (CMS Initial Mark 2) [1 CMS-initial-mark: 10812086K 3(11901376K) 4] 10887844K 5(12514816K) 6, 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]7

  1. 2015-05-26T16:23:07.321-0200: 64.425 - GC事件开始时间,包括时钟时间和相对于JVM启动时间
  2. CMS Initial Mark - CMS初始标记阶段,即收集所有的GC根
  3. 10812086K - 当前老年代的使用大小
  4. 11901376K - 老年代总大小
  5. 10887844K - 当前堆使用大小
  6. 12514816K - 堆总大小
  7. Times: user=0.00 sys=0.00, real=0.00 secs - 该阶段持续事件,按不同类别衡量
第二阶段:并发标记

在这个阶段,垃圾收集器会遍历老年代并标记所有活着的对象,从“初始标记”阶段找到的GC根开始。“并发标记”阶段,顾名思义,就是与应用程序线程并发运行,不会停止应用程序线程。请注意,此阶段并不是所有的老年代存活对象都可以被标记,因为应用程序在标记期间会改变引用。

2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark 1: 0.035/0.035 secs 2] [Times: user=0.07 sys=0.00, real=0.03 secs] 3

  1. CMS-concurrent-mark - CMS并发标记,遍历老年代并标记所有活着的对象
  2. 0.035/0.035 secs - 阶段持续时间
  3. Times: user=0.07 sys=0.00, real=0.03 secs - 该阶段时间没有意义,因为是从并发标记开始测量的,不仅仅包括为并发标记所做的工作
第三阶段:并发预清理

这又是一个并发阶段,与应用程序线程并行运行,而不是停止它们。由于前一阶段与应用程序线程同时运行,一些引用已被更改。每当发生这种情况时,JVM会将这些突变对象的区域标记为Dirty Card。

在预清理阶段,那些能够从Dirty Card区域可达的对象也被标记为存活,当标记完这些对象后,该Dirty Card区域就会消失。

2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean 1: 0.016/0.016 secs 2] [Times: user=0.02 sys=0.00, real=0.02 secs] 3

  1. CMS-concurrent-preclean - 并发预清理阶段,考虑到在前一阶段更改的引用
  2. 0.016/0.016 secs - 阶段持续时间
  3. Times: user=0.07 sys=0.00, real=0.03 secs - 该阶段时间没有意义,因为是从并发标记开始测量的
第四阶段:并发可中断预清理

与并发预处理清理类似,CMS有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。这两个参数组合起来的意思是预清理后,Eden空间使用超过2M时启动可中断的并发预处理,直到Eden空间使用率达到50%时中断,进入重新标记阶段。

2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean 1: 0.167/1.074 secs 2] [Times: user=0.20 sys=0.00, real=1.07 secs] 3

  1. CMS-concurrent-abortable-preclean - 并发可中断预清理阶段
  2. 0.167/1.074 secs - 阶段持续时间
  3. Times: user=0.20 sys=0.00, real=1.07 secs - 该阶段时间没有意义,因为是从并发标记开始测量的
第五阶段:最终标记

这是CMS垃圾收集的第二个也是最后一个stop-the-world暂停。这个阶段目标是完成老年代所有活动对象的标记。由于之前的预清理阶段是并发的,它无法跟上应用程序线程的变化速度,需要stop-the-world暂停才能完成。

通常CMS会在Young Generation尽可能发生后尝试运行“最终标记”阶段,以消除多个stop-the-world暂停。

2015-05-26T16:23:08.447-0200: 65.5501: [GC (CMS Final Remark2) [YG occupancy: 387920 K (613440 K)3]65.550: [Rescan (parallel) , 0.0085125 secs4]65.559: [weak refs processing, 0.0000243 secs5]65.559: [class unloading, 0.0013120 secs6]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs 7] [1 CMS-remark: 10812086K(11901376K)8] 11200006K(12514816K) 9, 0.0110730 secs10] [Times: user=0.06 sys=0.00, real=0.01 secs]11

  1. 2015-05-26T16:23:08.447-0200: 65.550 - GC事件开始时间,包括时钟时间和相对于JVM启动时间
  2. CMS Final Remark - CMS最终标记阶段,标记老年代所有活动对象,stop-the-world暂停
  3. YG occupancy: 387920 K (613440 K) - 年轻代使用量和总大小
  4. Rescan (parallel) , 0.0085125 secs - 重新扫描在应用程序线程停止时完成对活动对象的标记,在这种情况下,重新扫描是并行完成的,耗时0.0085125秒
  5. weak refs processing, 0.0000243 secs - 处理弱引用持续时间
  6. class unloading, 0.0013120 secs - 类卸载持续时间
  7. scrub string table, 0.0001759 secs - 清理类元数据和内部字符串表时间
  8. CMS-remark: 10812086K(11901376K) - 最终标记阶段后的老年代使用量和总量
  9. 11200006K(12514816K) - 最终标记阶段后堆的使用量和总量
  10. 0.0110730 secs - 最终标记阶段的持续时间
  11. Times: user=0.06 sys=0.00, real=0.01 secs - 暂停的持续时间,按不同类别衡量
第六阶段:并发清理

无需stop-the-world暂停,与应用程序线程同时运行。该阶段目标是清理未使用的对象并回收它们的空间。

2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep1: 0.027/0.027 secs2] [Times: user=0.03 sys=0.00, real=0.03 secs]3

  1. CMS-concurrent-sweep - 并发清理
  2. 0.027/0.027 secs - 阶段的持续时间
  3. Times: user=0.03 sys=0.00, real=0.03 - 该阶段时间没有意义,因为是从并发标记开始测量的
第七阶段:并发重置

清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集做准备。

2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset1: 0.012/0.012 secs2] [Times: user=0.01 sys=0.00, real=0.01 secs]3

  1. CMS-concurrent-reset - 并发重置
  2. 0.012/0.012 secs - 阶段的持续时间
  3. Times: user=0.01 sys=0.00, real=0.01 secs - 该阶段时间没有意义,因为是从并发标记开始测量的

总而言之,CMS垃圾收集器通过将大量工作分成不同阶段,在大部分阶段不需要stop-the-world暂停,从而减少延迟。然而,CMS也存在很多缺点,其中最明显的就是由于CMS使用“标记清除”算法清理老年代,所以会产生内存碎片;其次CMS并发清理阶段是和应用程序线程同时进行的,会产生浮动垃圾,CMS无法在当次的收集中处理掉它,只好等到下一次GC去处理;还有CMS并发清理失败后,会使用Serial Old收集器进行Full GC,由于Serial收集器是单线程、存在stop-the-world暂停,会出现长时间的暂停。

G1 - Garbage First

G1关键设计的目标之一是使垃圾收集导致的stop-the-world暂停持续时间变得可预测和可配置。实际上,G1是一个软实时的垃圾收集器,简单说就是您可以为stop-the-world配置一个毫秒长的时间,G1 GC将尽最大可能实现这个目标。

堆内存划分

为实现stop-the-world的时间可预测,G1将堆分成多个(通常大约2048个)可以容纳对象、大小相等的独立区域(Region),可以通过 -XX:G1HeapRegionSize 配置每个Region大小,每个Region大小可以从1MB到32MB不等,并且必须是 2 的幂。

每个Region可能是Eden,也可能是Survivor,也可能是Old,所有的Eden和Survivor区逻辑上组成年轻代,所有Old区逻辑上组成老年代。另外Region还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象都被认定为大对象,对于那些超过了整个Region容量的超级大对象,将被存放在多个连续的Humongous Region中。G1进行垃圾回收时,会将Humongous当成老年代回收。

可以通过在命令行指定以下参数启动此垃圾收集器:

java -XX:+UseG1GC com.mypackages.MyExecutableClass

Young GC

当JVM分配对象到Eden区域失败时,便会触发stop-the-world暂停多线程并行来进行年轻代的垃圾收集,YGC 将 Eden Region 中存活的对象拷贝到Survivor,或者直接晋升到Old Region中;将Survivor Regin中存活的对象拷贝到新的Survivor或者晋升Old Region。

Mixed GC

G1收集器很多理念是建立在CMS概念之上。G1并发标记使用Snapshot-At-The-Beginning(SATB或原始快照)方法标记周期开始时处于活动状态的对象,而CMS则采用增量更新。

当堆的整体占用足够大时,并发标记开始,默认情况下是45%,也可以通过设置 -XX:InitiatingHeapOccupancyPercent 参数进行修改,跟CMS类似,G1的并发标记由许多阶段组成,其中一些是与应用程序线程完全并发的,而另一些则需要停止应用程序线程。

第一阶段:初始标记

此阶段标记所有可从GC根直接访问的对象。它需要stop-the-world暂停。可以从日志 Evacuation Pause的“initial-mark”来查看:

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]
第二阶段:根区域扫描

这个阶段标记所有可从根区域访问的活动对象,即那些不为空的对象。此阶段与应用程序线程同时运行。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]
第三阶段:并发标记

这个阶段和CMS并发标记非常相似:遍历整个堆里的对象图,找到要回收的对象,此阶段与应用程序线程同时运行,并发标记时会产生漏标、错标问题,G1使用SATB算法来解决。

1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]
第四阶段:最终标记

此阶段会stop-the-world暂停,与CMS一样,完成最终的标记。对用户程序线程做短暂的暂停,用于处理并发标记阶段遗留下的SATB记录。

第五阶段:筛选回收

此阶段会计算堆中所有的活动对象,根据GC效率对这些区域排序,并按照-XX:MaxGCPauseMillis参数设定的毫秒数对价值最高的区域进行回收。这个阶段某部分是需要stop-the-world暂停的,例如标记初始标记以来所有对象的卡位图(TASM之上的所有对象)、为任何具有一个活动对象的区域标记区域位图、清理没有活动对象的区域RSet集等。

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

某部分是并发的,例如空区域回收和大部分活跃对象计算等。

1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

Full GC

如果Mixed GC在进行GC回收拷贝对象时,没有足够的空Region能够承载拷贝对象就会触发Full GC。Full GC是单线程的stop-the-world暂停,使用“标记整理”算法进行垃圾收集,这个过程是非常耗时的。