由于JDK的版本处于高速迭代中,Java从发展至今衍生了许多的GC版本,比如:Serial/Serial Old收集器、Parallel/Parallel Old收集器、CMS(Concurrent-Mark-Sweep)收集器,以及JDK7 Update4版本开始提供G1(Garbage-First)收集器等。

类型  名称 jvm的实现类
串行垃圾收集器 Serial收集器 def new generation 新生代
串行垃圾收集器 Serial Old收集器 tenured generation 老年代
并行垃圾收集器 ParNew收集器 par new generation 新生代
并行垃圾收集器 Parallel Scavenge收集器 PSYoungGen 新生代
并行垃圾收集器 Parallel Old收集器 ParOldGen 老年代
并发标记扫描垃圾收集器 CMS收集器 concurrent mark-sweep generation 老年代
G1垃圾收集器 G1收集器 garbage-first heap 均可

 

在正式了解垃圾收集器之前,先看看两个重要概念:

  • 串行还是并行回收:在同一时间段内只能执行一个线程,即使度讴歌CPU可用时,也只能有一个CPU用于执行垃圾回收操作,并且在执行垃圾回收时,程序中的所有工作线程将会被暂停(Stop-the-world),当垃圾收集工作完成后才会恢复之前被暂停的工作线程,这就是串行回收。JVM的client的模式下,内存开销相对于Server模式更小,因此串行回收默认应用在Client模式中。和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,提升应用程序的吞吐量,不过仍然使用了Stop-the-world机制和复制算法。
     
  • 并发还是Stop-the-world机制:当通过Stop-the-world机制来运行垃圾收集器,GC会在运行过程中暂停程序中所有工作线程,直到完成内存回收后,才回复之前被暂停的工作线程。Stop-the-world出现在新生代的Minor GC中,由于新生代的内存空间通常都比较小,所以暂停时间也在可接受的范围内;不过一旦出现在老年代的Full GC中,程序的工作线程会被暂停很久,这跟Java对空间的内存大小有关。简单来说,内存空间越大,执行Full GC的时间就会越久,相对的工作线程被暂停的时间也更长。但是一次Full GC执行时间过程,会严重影响到程序的正常运行,因此JVM设计者提供了并发回收,在同一时间段内,应用程序的工作线程和垃圾收集线程将会同时运行或者交叉运行,希望以此缩短Stop-the-world的暂停时间。尽管垃圾收集器越来越优秀,回收效率也越来越高,但也只是尽可能的缩短暂停时间而已,做不到完全不需要Stop-the-world。

 

串行回收:Serial收集器

       Serial收集器作用于新生代中,它采用复制算法串行回收“Stop-the-world”机制的方式执行内存回收。在早年硬件限制的JDK版本中,CPU受限于单个,使用Serial收集器执行新生代垃圾收集几乎是唯一的选择,并且Serial收集器缺省也作为HotSpot中Client模式下的新生代垃圾收集器。

       除了新生代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和“Stop-the-World”机制,只不过内存回收算法使用的却是标记—压缩算法。如果JVM受限在单个CPU的宿主环境中,使用Serial收集器+Serial Old收集器的组合执行Client模式下的内存回收将会是不错的选择。基于串行回收的垃圾收集器适用于大多数对暂停时间要求不高的Client模式下的JVM中,由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销,虽然执行串行回收会降低程序的吞吐量,但是回收质量还是不错的。

参数设置:

  • -XX:+UseSerialGC:手动指定使用Serial收集器执行内存回收任务。

 

并行回收:ParNew收集器

       ParNew收集器作用于新生代中,它采用复制算法并行(多线程)回收和“Stop-the-world”机制的方式执行内存回收。ParNew收集器运行在多CPU的宿主环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更迅速地完成垃圾收集,提升程序的吞吐量。

       但是如果是在CPU受限于单个的情况下,ParNew收集器不见得会比Serial收集器更高效,虽然Serial收集器是基于串行回收,但由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。所以从理论上来说,Serial收集器的优势是在JVM受限于单CPU的宿主环境中,而ParNew收集器的优势则是体现在多CPU、多核心等宿主环境中,并且在某些注重低延迟的应用场景中,ParNew收集器+CMS(Concurrent-Mark-Sweep)收集器组合执行Server模式下的内存回收几乎是最佳选择。

参数设置:

  • -XX:+UseParNewGC : 手动指定使用ParNew收集器执行内存回收任务。

 

程序吞吐量优先:Parallel收集器

Parallel收集器采用了复制算法并行回收和“Stop-the-World”机制。和ParNew收集器不同,Parallel收集器可以控制程序的吞吐量大小,因此它也被称作吞吐量优先的垃圾收集器。

参数设置:

  • -XX:GCTimeRatio:设置执行内存回收的时间所占JVM运行总时间的比例,也就是控制GC的执行频率,公式为1/(1+N),默认值为99,也就是说,将只有1%的时间用于执行内存回收。
  • -XX:MaxGCPauseMillis:设置执行内存回收时“Stop-the-World”机制的暂停时间阈值,如果指定了该选项,那么Parallel收集器将会尽可能地在设定的时间范围内完成内存回收。

 

       在此大家需要注意,在垃圾收集器中吞吐量和低延迟这两个目标其实是存在相互竞争的矛盾,因为如果选择以吞吐量优先,那么降低内存回收的执行频率则是必然的,但这将会导致GC需要更长的暂停时间来执行内存回收。相反如果选择以低延迟优先,那么为了降低每次执行内存回收时的暂停时间,只能够频繁地执行内存回收,但这又引起了新生代内存的缩减和导致程序吞吐量的下降。举个例子,在60s的JVM总运行时间里,每次GC的执行频率是20s/次,那么60s内一共会执行3次内存回收,按照每次GC耗时100ms来计算,最终一共会有300ms(即60/20*100)被用于执行内存回收。

       但是如果我们将选项“-XX:MaxGCPauseMillis”的值调小后,新生代的内存空间也会自动调整,相信大家都知道,内存空间越小就越容易被耗尽,那么GC的执行频率就会更频繁。之前在60s的JVM总运行时间里,最终会有300ms被用于执行内存回收,而如今GC的执行频率却是10s/次,60s内将会执行6次内存回收,按照每次GC耗时80ms来计算,虽然看上去暂停时间更短了,但最终一共会有480ms(即60/10*80)被用于执行内存回收,很明显程序的吞吐量下降了。所以大家在设置这两个选项时,一定需要注意控制在一个折中的范围之内。

参数设置:

  • -XX:UseAdaptiveSizePolicy:设置GC的自动分代大小调节策略,一旦设置这个选项后,就意味着开发人员将不再需要显示的设置新生代中的一些细节参数了,JVM会根据自身的当前运行情况动态调整这些相关参数。

       和Serial收集器一样,Parallel收集器也提供用于执行老年代垃圾收集的Parallel Old收集器,Parallel Old收集器采用了标记—压缩算法,但同样也是基于并行回收和“Stop-the-World”机制。在程序吞吐量优先的应用场景中,Parallel收集器+Parallel Old收集器组合执行Server模式下的内存回收将会是不错的选择。

参数设置:

  • -XX:+UseParallelGC:手动指定使用Parallel收集器执行内存回收任务。

 

低延迟:CMS(Concurrent-Mark-Sweep)收集器

       在某些对系统响应速度要求比较高的项目中,大家总是希望系统能够快速做出响应,而不愿意看到过多的延迟。基于低延迟的考虑,JVM的设计者们提供了基于并行回收的CMS(Concurrent-Marking-Sweep)收集器,它是一款优秀的老年代垃圾收集器,也可以称作Mostly-Concurrent收集器。CMS天生为并发而生,低延迟是它的优势,不过垃圾收集算法却并没有采用标记—压缩算法,而是采用标记—清除算法,并且也会因为“Stop-the-World”机制而出现短暂的暂停。

CMS的实现细节执行过程主要可以划分为4个阶段:

  • 初始标记(Initial-Mark)阶段:
    在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务就是标记出内存中那些被根对象集合所连接的目标对象是否可达,一旦标记完成之后就会恢复之前被暂停的所有应用线程。
  • 并发标记(Concurrent-Marking)阶段:
    接下来将会进入并发标记阶段,而这个阶段的主要任务就是将之前的不可达对象标记为垃圾对象。
  • 再次标记(Remark)阶段:
    在CMS最终执行内存回收之前,尽管看上去这些垃圾对象都已经被成功标记了,但是由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此在并发标记阶段将无法有效确保之前被标记为垃圾的无用对象的引用关系遭到更改,为了解决这个问题,CMS会进入到再次标记阶段,这样一来,程序会因为“Stop-the-World”机制而再次出现短暂的暂停,以确保这些垃圾对象都能够被成功且正确地标记。
  • 并发清除(Concurrent-Sweep)阶段:
    当经历过初始标记、并发标记和再次标记三个阶段后,CMS最终将会进入到并发清除阶段执行内存回收,释放掉无用对象所占用的内存空间。

尽管CMS收集器采用的是并行回收,但是在其初始标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是尽可能地降低暂停时间而已。

CMS收集器的垃圾收集算法采用的却是标记—清除,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片,那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

       在HotSpot中,当垃圾收集器执行完内存回收后,如果内存空间中产生内存碎片,那么则只能够选择空闲列表作为内存分配算法为新对象分配内存空间。简单来说,会有JVM负责维护一个列表,其中所记录的内容就是当前内存空间中可用内存块的坐标,当执行内存分配时,会从列表中定位到一个与新对象所需内存大小一致的连续内存块用于存储生成的对象实例。考虑到内存碎片存在的弊端,CMS收集器提供选项“-XX:+UseCMS-CompactAtFullCollection”,用于指定在执行完Full GC后是否对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间将会变得更加长,因此CMS收集器还提供另外一个选项“-XX:CMSFullGCs-BeforeCompaction”,用于设置在执行多少次Full GC后对内存空间进行压缩整理。除了会产生内存碎片外,CMS收集器还存在一个不容忽视的问题,那就是在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象时,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时地回收掉,从而只能够在下一次执行GC时,释放掉这些之前未被回收的内存空间。

       在此大家需要注意,尽管Full GC几乎已经成为老年代垃圾回收的代名词,但实际上Full GC的回收范围却不单单仅限于老年代中,从严格意义上来说,Full GC的回收范围几乎覆盖了整个堆空间,因此Full GC将会比Minor GC耗费更长的时间来完成垃圾收集。在HotSpot中,除了CMS收集器之外的任何其他老年代垃圾收集器在执行内存回收时,都将会执行Full GC(G1收集器的垃圾收集动作比较特殊,被称之为Mixed GC),而CMS收集器却提供选项“-XX:CMSInitiatingOccupancyFraction”用于设置当老年代中的内存使用率达到多少百分比的时候执行内存回收(低版本的JDK缺省值为68%,JDK6及以上版本默认值则为92%),这里的内存回收范围仅限于老年代,而非整个堆空间,因此通过该选项便可以有效降低Full GC的执行次数。当然并不是说使用了CMS收集器之后,就永远不会再触发Full GC了,一旦CMS在执行过程中出现“Promotion Failed”或“Concurrent Mode Failure”时,将仍然有可能会触发Full GC操作。

参数设置:

  • -XX:+UseConcMarkSweepGC:来手动指定使用CMS收集器执行内存回收任务。
  • -XX:+UseCMS-CompactAtFullCollection:用于指定在执行完Full GC后是否对内存空间进行压缩整理,以此避免内存碎片的产生。
  • -XX:CMSFullGCs-BeforeCompaction:用于设置在执行多少次Full GC后对内存空间进行压缩整理。

 

区域化分代式:G1(Garbage-First)收集器

       G1收集器的设计初衷是为了替代CMS收集器而生,简单来说,G1是一款基于并行和并发、低延迟以及暂停时间更加可控的区域化分代式垃圾收集器。和上面介绍过的所有垃圾收集器都不同(包括CMS收集器),G1在设计上的改变可以说是具有革命性的意义。首先在内存空间的设计上,G1重新塑造了整个Java堆区,尽管同样也是基于分代的概念执行内存分配和垃圾回收,但是G1却并没有采用传统物理隔离的新生代和老年代布局方式(仅逻辑上划分为新生代和老年代),而是选择将Java堆区划分成约2048个大小相同的独立Region块,每个Region块之间可能是不连续的,其大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,如图6-13所示。这样划分的好处在于可以更好地提升GC的回收效率和缩短“Stop-the-World”机制的暂停时间以换取更大的程序吞吐量,甚至能够真正做到精准控制程序的暂停时间等。这是因为G1收集器在执行内存回收时,会优先释放掉整个Java堆区中一些占用内存较大的Region块,从而可以避免像以往一样直接扫描整个Java堆区(如果一个Region块中引用另一个Region块内的对象时,则通过Remembered Set技术避免全堆扫描)。至于执行内存回收时“Stop-the-World”机制的暂停时间更加可控,是相对于Parallel收集器而言,尽管Parallel收集器也提供选项“-XX:MaxGCPauseMillis”控制程序的暂停时间,但是在大内存的空间释放操作中,暂停时间其实并非是完全可控的,只能说是尽可能控制在预期范围内,但谁也无法保证内存回收时所带来的暂停时间一定会百分百地控制在规定的时间阈值内,所以必然会有一定的误差。而由于G1收集器并非全堆扫描,只优先回收占用内存较大的一些Region块,所以G1收集器的暂停时间会更加可控。

G1收集器的执行过程主要可以划分为6个阶段,如下所示:

  • 初始标记(Initial-Mark)阶段:
    在初始标记阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,该阶段的主要任务是标记Root-Region。一旦标记完成之后就会恢复之前被暂停的所有应用线程。

  • 根区域扫描(Root-Region-Scanning)阶段:
    接下来将会进入到根区域扫描阶段,该阶段的主要任务是扫描Root-Region中引用老年代的一些Region块,只有在执行完该阶段之后,才能开始下一次新生代内存回收。

  • 并发标记(Concurrent-Marking)阶段:
    并发标记阶段的主要任务就是找出整个Java堆区中的存活对象,由于该阶段并不会导致程序出现暂停,因此在执行的过程中允许被新生代内存回收打断。

  • 再次标记(Remark)阶段:
    再次标记阶段和初始标记阶段同样都是基于“Stop-the-World”机制的,该阶段的主要任务就是完成整个堆区中存活对象的标记。

  • 清除(Cleanup)阶段:
    清除标记阶段由三部分构成,首先会计算出所有的活跃对象并完全释放一些自由的Region块,然后处理Remembered Set,在此大家需要注意,这两部分操作将会暂停程序中的应用线程,然后并发重置空闲的一些Region块,并将它们放回至空闲列表中。

  • 拷贝(Copying)阶段:
    最后的拷贝阶段也是基于“Stop-the-World”机制的,该阶段的主要任务是将存活对象复制到未使用过的Region块中。

尽管G1收集器长期以来的目标是为了逐步替代掉CMS收集器,但这并非是要求大家立刻将生产环境中JDK版本升级到JDK7 Update4及更高的版本,或者将CMS收集器切换到G1收集器,如果目前程序中并没有因为“Stop-the-World”机制导致程序出现长时间的暂停,应该尽可能保持当前现状不变,避免因此导致程序出现一些无法预估的风险。

参数设置:

  • -XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务。

 

内存选项配置


 

GC组合配置

 

GC选项配置

最后修改于 2019-11-25 09:21:58
如果觉得我的文章对你有用,请随意赞赏
扫一扫支付
上一篇