第 5 章 垃圾收集器和内存分配

  目前,Java中支持的垃圾收集器其有四种:串行回收器、并行回收器、CMS回收器、G1回收器。

一、一心一意一件事:串行垃圾收集器

  串行回收器是指使用单线程进行垃圾回收。对于并行能力较弱的计算机,串行回收器的专注性和独站性往往有更好的性能表现。串行回收器可以在新生代和老年代使用,分为新生代串行垃圾回收器,和老年代串行垃圾回收器。

新生代串行收集器

  串行收集器是所有垃圾回收器中最古老的一种,也是JDK中最基本的垃圾回收器,串行回收器有两个特点:
  第一、它仅使用单线程进行垃圾回收
  第二、它是独占式的垃圾回收
  串行回收器在工作时会产生“Stop-The-World”现象,在实时性要求较高的场景中可能不被接受,但即便如此,串行回收器却是一个成熟且经过长时间生产环境考验的极为高效的收集器。由于不需要线程切换,在诸如单CPU等硬件平台不是特别优越的场合,它的性能表现可以超过并行回收器和并发回收器。
  使用-XX:+UseSerialGC 参数可以指定使用新生代和老年代串行收集器。且当虚拟机工作在 Client 模式下时,其默认的也是串行垃圾收集器。

老年代串行收集器

  老年代串行收集器使用的是标记压缩算法。它也是一个独占式的垃圾回收器。由于老年代的特性,其使用复制算法并不划算,再由于老年代一般空间较大,因此一旦老年代串行收集器启动,应用很可能因此停顿较长时间。
  可通过以下几种方法启用老年代串行回收器
  (1)-XX:+UseSerialGC:新生代,老年代都使用串行回收器
  (2)-XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器
  (3)-XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器

二、人多力量大:并行回收器

  并行回收器使用多个线程同时进行垃圾回收。对于并行能力强的计算机,可以有效缩短垃圾回收所需的时间。

新生代ParNew回收器

  ParNew回收器是一个工作在新生代的垃圾收集器。它只是简单地将串行回收器多线程化。ParNew也是独占式回收器,但由于使用多线程进行回收,因此,在并发能力较强的CPU上,产生的停顿时间要短于串行回收器。而在单CPU或并发能力较弱的计算机中,由于多线程的压力,并行回收器的表现很可能比串行回收器差。
  开启ParNew回收器可以使用以下参数
  (1)-XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器
  (2)-XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS回收器
  ParNew工作时的线程数量,可以使用 -XX:ParallelGCThreads 指定。一般,最好与CPU数量相当,避免线程数过多,影响收集性能。在默认情况下,当CPU小于8个时,ParallelGCThreads 等于CPU数量,当CPU大于8个时,ParallelGCThreads 等于 3+((5*CPU_Count)/8)。

新生代ParallelGC回收器

  新生代ParallelGC也是使用复制算法。和ParNew一样,都是多线程,独占式。但ParallelGC有一个重要的特点:它非常关注系统的吞吐量。
  新生代ParallelGC回收器可以使用以下参数启用
  -XX:+UseParallelGC:新生代使用ParallelGC,老年代使用串行回收器。
  -XX:+UseParallelOldGC:新生代使用ParallelGC,老年代使用ParallelOldGC。
  ParallelGC回收器提供了两个重要参数控制系统吞吐量。
  -XX:MaxGCPauseMillis:最大垃圾收集停顿时间,一个大于 0 的整数。会使ParallelGC在工作时自动调整Java堆大小或者其他一些数据,尽可能把停顿时间控制在MaxGCPauseMillis以内。如果希望减小停顿时间,而反这个值设置很小,虚拟机可能会使用一个较小的堆,从而导致垃圾回收变得很频繁,而增加了垃圾回收的总时间,降低了吞吐量。
  -XX:GCTimeRatio:吞吐量大小。一个 0 到 100 之间的整数。假设GCTimeRatio的值为 n,则系统将花费不超过 1/(1+n)的时间用于GC。默认值为 99,即不超过 1/(1+99)=1%的时间用于垃圾收集。
  此外,与ParNew的另一个不同在于,它还支持一种自适应的GC调节策略。使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。这种模式下新生代大小,eden和survivor的比例,晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆,目标吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
  -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio 可以设置期望的停顿时间和吞吐量。但是鱼和熊掌不可兼得,这两个参数是相互矛盾的,通常如果减少一次收集的最大停顿时间,就会同时减少系统吞吐量,增加系统吞吐量以可能会同时增加一次垃圾回收的最大停顿。

老年代ParallelOldGC回收器

  老年代ParallelOldGC回收器也是一种多线程并发收集器。和新生代ParallelGC回收器一样,它也是一种关注吞吐量的收集器。名称中间的Old表示这是一个应用于老年代的回收器。
  ParallelOldGC回收器使用标记压缩算法,参数 -XX:UseParallelOldGC可以在新生代使用 ParallelGC,老年代使用ParallelOldGC回收器。这是一对非常关注吞吐量的垃圾回收器组合。在对吞吐量敏感的系统中,可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。

三、一心多用都不落下:CMS回收器

  与 ParallelGC 和 ParallelOldGC 不同,CMS回收器主要关注于系统停顿时间。CMS 是 Concurrent Mark Sweep的缩写,意为并发标记清除,即使用了标记清除算法的多线程并行垃圾回收器。

CMS主要工作步骤

  CMS回收器工作过程与其他垃圾收集器相比略显复杂。主要步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记为独占系统资源,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行的。因此,从整体上看,CMS不是独占式的,它的部分步骤可以和用户线程同时进行。
  预清理过程是并发的(-XX:-CMSPrecleaningEnabled 可以关闭预清理),除了为正式清理做准备和检查以外,预清理还会尝试控制一次停顿时间。由于重新标记是独占CPU的,如果新生代GC发生后,立即触发一次重新标记,那么一次停顿时间可能很长。为了避免这种情况,预处理时,会刻意等待一次新生代GC的发生,然后根据历史数据预测下一次新生代GC最不可能发生的时间,如中间时刻,进行重新标记。以此,尽可能减少一次停顿时间。

CMS主要参数

  启用CMS回收器的参数是-XX:UseConcMarkSweepGC。CMS是多线程回收器,设置合理的工作线程数量也对系统性能有重要的影响。
  CMS默认启用的并发线程数=(ParallelGCThreads +3)/ 4。ParallelGCThreads表示GC并行时使用的线程数量,如果新生代使用ParNew,那么ParallelGCThreads也就是新生代GC的线程数量。这意味着当有4个ParallelGCThreads时,只有 1个并发线程,而当有两个并发线程时,实际已有 5 ~ 8个ParallelGCThreads 线程数。
  并发线程数量可以通过 -XX:ConcGCThreads 或者 -XX:ParallelCMSThreads 参数手工设定。当CPU资源比较紧张时,受到 CMS回收器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。
  并发是指收集器和应用线程交替执行,并行是指应用程序停止,同时由多个线程一起执行GC。因此并行回收器不是并发的。因为并行回收器执行时,应用程序完全挂起,不存在交替执行的步骤。
  CMS在回收过程中,应用仍然在不停地工作,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。CMS不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始回收,以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。这个阈值可以使用 -XX:CMSInitiatingOccupancyFraction 来指定,默认 68。即当老年代的空间使用率达到 68%时,会执行一次 CMS回收。如果应用内存使用率增长很快,在CMS过程中已经出现了内存不足的情况,此时,CMS回收会失败,虚拟机将启动老年代串行收集器进行垃圾回收。如果这样,应用程序将完全中断,直到垃圾回收完成,这时,应用程序的停顿时间可能会较长。
  对 -XX:CMSInitiatingOccupancyFraction 的调优思路是:如果内存增长缓慢,则可以设置一个稍大的值,以有效降低 CMS的触发频率,改善应用性能;如果应用内存增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
  CMS 是一个基于标记清除算法的回收器,在前面也讲过这会造成大量内存碎片,这种情况下,即使堆内存仍然有较大的剩余空间,也可能会被迫进行一次垃圾回收,以换取一块可用的连续内存。这对系统性能是相当不利的,CMS 为此提供了几个用于内存压缩整理的参数。
  -XX:+UseCMSCompactAtFullCollection 开关可以使 CMS在垃圾收集完成后,进行一次内存碎片整理,但该过程不是并发进行的。-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次 CMS 回收后,进行一次内存压缩。

有关 Class 的回收

  虚拟机在启动后会扫描所有class文件到永久区,CMS 在回收 Perm 区时,默认情况下,还是需要触发一次 Full GC。
  如果希望使用 CMS 回收 Perm 区,则必须打开 -XX:+CMSClassUnloadingEnable 开关,之后,如果条件允许,那么系统会使用 CMS的机制回收 Perm 区 Class 数据。

四、未来我做主:G1 回收器

  G1回收器(Garbage-First)是在JDK 1.7中正式使用的全新垃圾回收器,目标是取代 CMS,G1拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。G1依然属于分代垃圾回收器,会区分年轻代和老年代,,依然有 eden区和 survivor区,但从堆结构上看,它并不要求整个 eden 区、年轻代或者老年代都连续。作为 CMS 的长期替代方案,G1 使用了全新的分区算法,特点如下:
  并行性:G1 回收期间,可以由多个线程同时工作,有效利用多核计算能力
  并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用同时执行,因此一般来说,不会在整个回收期间完全阻塞应用程序
  分代 GC:G1 依然是一个分代收集器,但和之前不同,它同时兼顾年轻代和老年代。 这里是一个很大的不同。
  空间整理:G1 在回收过程中,会进行适当的对象移动,不像 CMS,只是简单地标记清理,在若干次GC 后,CMS 必须进行一次碎片整理。而 G1 不同,它每次回收都会有效地复制对象,减少空间碎片。
  可预见性:由于分区的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

G1 的内存划分和主要收集过程

  G1 收集器将堆进行分区,每次只收集其中几个区域,以此来控制垃圾回收产生的一次停顿时间。
  G1 的收集过程有 4个阶段:
   - 新生代 GC
   - 并发标记周期
   - 混合收集
   - 如果需要,可能会进行 Full GC

G1 的新生代 GC

  新生代GC 的主要工作时回收 eden 区和 survivor区。一旦 eden 区被占满,新生代 GC 就会启动。

G1可用的命令行选项有:

   -XX:+UseG1GC——让JVM使用G1垃圾回收器
   -XX:MaxGCPauseMillis=200——设置GC暂停时间目标值,缺省200毫秒。但这不是硬指标,JVM会尽力满足。
   -XX:InitiatingHeapOccupancyPercent=45——整个堆被占用多少之后开始进行GC,缺省为45,0表示持续不停进行GC
   -XX:NewRatio=n——年轻代和老年代的比例,缺省为2
   -XX:SurvivorRatio=n——Eden和Survivro的比例,缺省为8
   -XX:G1ReservePercent=n——保留的堆大小,减少晋升过程中出错的可能性,也就是增加可用的to-space内存,缺省是10
   -XX:G1HeapRegionSize=n——G1中,堆分为大小相等的区域。这个参数设置区域的大小,缺省值取决于堆的总大小,有效取值是1M-32M。

最佳实践

使用G1时的最佳实践

1、不要设置年轻代的大小(-Xmn),否则会扰乱G1的缺省行为,JVM也不会满足用户指定的暂停时间。而且设置了固定值的话,G1将无法随需扩展年轻代的大小

2、GC暂停时间不是100%能保证的

3、如果GC的晋升过程中遇到堆区域溢出(使用-XX:+PrintGCDetails看到to-space overflow),可以通过下面几种方式避免:

增加-XX:G1ReservePercent=n,缺省值是10。这可以增加可用的to-space内存
使用-XX:ConcGCThreads=n增加标记线程数目

完整的G1命令行选项

下面给出G1的完整命令行选项,使用时,请记住上面的最佳实践。

选项和默认值 描述
-XX:+UseG1GC 使用G1收集器
-XX:MaxGCPauseMillis=n 设置一个预期的停顿时间,记住这只是个软目的,JVM会尽力去实现
-XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45.
-XX:NewRatio=n 新生代和老年代的大小比例(new/old),默认是2
-XX:SurvivorRatio=n eden/suvivor的比例,默认是8
-XX:MaxTenuringThreshold=n 对象晋升的年龄,默认是15
-XX:ParallelGCThreads=n 收集器并发阶段使用的线程数。默认值是取决于JVM运行的平台
-XX:ConcGCThreads=n 设置收集器的线程数。默认值是取决于JVM运行的平台
-XX:G1ReservePercent=n 设置G1保留内存,防止转移失败
-XX:G1HeapRegionSize=n G1收集器把堆内存细分成很多个大小一致的小区域。这个选项是设置每个区域的大小默认值是根据堆的总量,算出的。范围是1 Mb ~ 32 Mb

猜你喜欢

转载自blog.csdn.net/xuguangyuansh/article/details/78637178