JVM(三) 垃圾回收时间点和垃圾收集器

        收集器组合章节来自第一篇参考文章,非原创,作者总结地非常好!

         分代收集相关概念来自参考文章第二篇,非原创

        第二篇参考资料的文章质量很高,推荐阅读!

分代收集(Generational Collection)相关概念

      在Java8的HotSpot虚拟机中一共包括了5个垃圾收集器,它们每一个都是基于分代收集的思想。在这一节中,我主要介绍一下各个分代区域以及对象是怎样被分配到这些区域的。这是官方文档给出的5个可得到的收集器:5 Available Collectors,并介绍了如何针对自己的应用选择出一个合适的收集器。

 

Generational Hypothesis

        对于Generational Hypothesis的概念,Jeff Hammerbacher在Quora上已经给出一个很好地答案,我把它翻译一下。

Generational Hypothesis是一个关于对象生命周期分布的假设。准确地说,这个假设认为对象生命周期的分布是双峰的:大部分的对象要么是短命的,要么就是一直存活的。

Generational Hypothesis

Generational Hypothesis

       基于这个假设,Hotspot虚拟机把内存分为年轻代(Young Generation)和老年代(Old Generation)。有了这样的内存区域划分,我们可以针对不同的区域选择合适的算法来进行垃圾收集,从而大大提高垃圾收集的效率。注意:分代收集是基于上面的假设来进行的,如果你的应用完全不符合上面的假设,那么你的垃圾收集效率一定很低。

       因为年轻代空间通常很小,包含很多短命的对象,所以它的收集要频繁一些。经过了几轮年轻代收集,依然存活的对象被晋升(promoted)或者tenured到老年代。因为老年代的空间要比年轻代大很多并且它的对象大部分都是长命的,所以它的收集是不频繁的。由于年轻代的收集很频繁,因此针对这个区域的收集算法要很快。另一方面,由于老年代的收集不是很频繁的并且它占用了大多数的堆空间,因此这一区域的算法针对低频的垃圾收集要空间有效的。

        在介绍各个分代区域之前,大家先看看下面这张图。

Generational Collection

Generational Collection

注意:在Java 8中已经移除了永久代。

 

年轻代

      年轻代是由一个Eden区域 + 2个survivor区域组成。大部分的对象最初都被分到Eden区域(特别大的对象可能直接被分配到老年代)。对于2个survivor区域来说,它们中的一个必须始终是空的。并且每个survivor区域中的对象至少是经历过一次年轻代垃圾收集的。假设几分钟前垃圾收集器已经进行了一次年轻代的垃圾收集了,Eden区域和其中的1个survivor区域都有对象,另一个survivor区域为空。现在,又要进行一次垃圾收集了,收集器做的就是:把Eden区域和那个有对象的survivor区域中活着的对象找出来并复制到另一个空的survivor区域中,然后清空Eden区域和先前有对象的那个survivor区域。

       如果空的这个survivor区域的空间不够装下Eden区域和另一个survivor区域中活着的对象,那么收集器会把容纳不下的对象直接分配到老年代。如果老年代也容不下这些对象,那么会触发老年代的垃圾收集,然后去容纳这些对象。

        由于Java应用支持多线程,那么在多线程应用的情况下对象的分配就会出现一些问题。比如,我上一个线程分配的对象会被下一个线程所分配的对象覆盖。如果用一个全局锁来把整个年轻代锁住,那么分配一个对象到年轻代的性能会非常低下。因此,虚拟机团队想出了一个解决方案叫做Thread-Local Allocation Buffers (TLABs).

 

Thread-Local Allocation Buffers

Thread-Local Allocation Buffers

       如上图所示,每一个线程都有一个自己的TLAB,分配对象时用指针碰撞(bump-the-pointer)技术去在各自的TLAB中分配对象,大大提升了年轻代分配对象的效率。设置‐XX:+UseTLAB来启用TLAB,通过‐XX:TLABSize来设置其大小,默认大小为0,0表示虚拟机动态计算其大小。

       经过了几次垃圾收集还没有被回收的对象就被promoted到老年代了。那么如何去判断一个对象是否足够老可以晋升到老年代呢?垃圾收集器追踪每个活着对象被收集的次数,每挺过一次垃圾收集,对象的年龄就加1,当一个对象的年龄超过了指定的阙值(tenuring threshold),它就会被晋升到老年代。通过设置XX:+MaxTenuringThreshold来指定一个上限,如果设置为0,那么经过1次垃圾收集以后马上被晋升。

 

老年代

      老年代的空间是非常大的并且它里面存在的对象成为垃圾的可能性很小。老年代的垃圾收集次数要比年轻代少很多,并且由于老年代的对象很少会成为垃圾对象,年轻代的做法(在survivor区域不断copy)并不适合老年代。老年代具体的收集算法我会在下面具体的垃圾收集器中介绍。

 

永久代

        永久代在Java 8以前存在。 JVM用这里存储一些类的元数据还有一些被内在化的字符串。What is String interning?详细地解释了什么是内在化字符串。Hotspot虚拟机用永久代实现了方法区,因此如果你用动态代理技术或CGLib产生大量的增强代理类,都会使永久代出现异常。比如,当你用Spring的AOP时,它都会为想要增强的类产生一个代理类从而达到增强的目的,如果产生的类很多,你的永久代将会溢出。

永久代给Java开发者制造了很多的麻烦,因为很难预测出它将需要多少内存空间。如果出现溢出:产生java.lang.OutOfMemoryError: Permgen space.的错误。

 

Metaspace

        由于上面永久代的缺点,它在Java 8中被移除,取而代之的是Metaspace,这块内存区域位于本地内存中。默认情况下,Metaspace的大小只被Java进程可得到的本地内存所限制。因此,这个区域并不会因为稍微增加一个类就导致溢出。注意:Metaspace没有限制地增长将会导致本地内存溢出。 你可以设置-XX:MaxMetaspaceSize来限制其大小。

HotSpot 垃圾回收时间点

枚举根节点

       垃圾回收必须要枚举到根节点经过判断才可以知道哪些对象是存活的,于是就会有以下的问题了:

  • 根节点那么多,逐个检查需要时间,用什么来记录需要回收的根节点和相关的引用,即枚举根节点
  • 多个线程必须在同个时间点停下,接受垃圾回收,这个时间点应该在哪里呢
  • 应该如何中断正在执行的线程,当要GC 时所有线程一起停下来吗?
  • 有的线程是Sleep 或是 Blocked 状态时,收不到GC 发过来的标志信号,那怎么办呢?

        第一个问题,JVM 使用了一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot 就把对象内存多少偏移量上是什么类型的数据计算出来(有点像java中的unsafe类的方法,在AQS中或是concurrenthashmap中),在JIT编译过程中在特定位置纪录下栈和寄存器中哪些位置是引用。

        第二个问题,这个时间点称之为 “Stop The World”----Safepoint,这个时间点在程序中的选定要是太少,那么GC 就要等待太久,要是太多个安全点,那么GC 次数变多,容易引起性能问题,所以安全点的选定基本上是以“是否具有让程序长时间执行的特征”,具有这类的最明显的特征就是指令序列复用,例如方法调用,循环跳转,异常跳转等。

        第三个问题, JVM 使用主动式中断( Voluntary Suspension ),当要GC时,仅仅设置一个标志位,让线程自己去主动轮询这个标志,发现中断标志为真时,就自己挂起,轮徐标志的地方和安全点是重合的。

        第四个问题,JVM设定了一个“安全区域(Safe Region)”,即在这个范围内,引用关系不会裱花,我们也可以把Safe Region 看做是被扩展的 Safe Point.

 

垃圾收集类型

由于Hotspot虚拟机的垃圾收集是基于分代思想的,那么在不同的分代区域收集会产生不同的垃圾收集类型,本节我将会介绍这些垃圾收集类型以及它们发生的时机。

 

minor gc

发生在年轻代的垃圾收集叫做minor gc,它具体的细节是什么样呢?

  • 当JVM不能为一个新对象分配空间时,minor gc被触发。例如:Eden区域被填满时。因此,你的应用分配对象的频率越高,minor gc发生的越频繁。
  • 在minor gc期间,老年代实际上被忽略。因此,从老年代到年轻代的引用被当作GC roots,而从年轻代到老年代的引用在标记阶段被忽略。
  • minor gc会触发stop-the-world的发生,致使应用线程停止。如果在Eden区域中的大部分对象都被标记为垃圾,既符合上面的假设,那么停顿时间是可以忽略不计的。但是,如果与假设相反,在Eden区域依然大部分的对象都是活着的,那么minor gc会花费很多的时间。

full gc

        清理整个堆的过程叫做full gc,有时也叫做major collection. 当老年代太满了而不能要接受所有来自年轻代晋升的对象时,所有的收集器(除了CMS)将停止年轻代的收集算法运行,而是用老年代的收集算法清理整个堆内存。(CMS垃圾收集器的老年代收集算法不能收集年轻代)。

  

垃圾收集器

         先上一张图。

_thumb

概述

七种垃圾收集器

  1. Serial(串行GC)-复制
  2. ParNew(并行GC)-复制
  3. Parallel Scavenge(并行回收GC)-复制
  4. Serial Old(MSC)(串行GC)-标记-整理
  5. CMS(并发GC)-标记-清除
  6. Parallel Old(并行GC)--标记-整理
  7. G1(JDK1.7update14才可以正式商用)

说明:

  1. 1~3用于年轻代垃圾回收:年轻代的垃圾回收称为minor GC
  2. 4~6用于年老代垃圾回收(当然也可以用于方法区的回收):年老代的垃圾回收称为full GC
  3. G1独立完成"分代垃圾回收"

注意:并行与并发

  1. 并行:多条垃圾回收线程同时操作
  2. 并发:垃圾回收线程与用户线程一起操作

常用五种组合

  1. Serial/Serial Old
  2. ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已
  3. ParNew/CMS:当下比较高效的组合
  4. Parallel Scavenge/Parallel Old:自动管理的组合
  5. G1:最先进的收集器,但是需要JDK1.7update14以上

Serial 和 Serial Old 收集器

         从图中可以看出,前者在新生代,后者在老生代,Serial只有一个线程去收集,并且必须暂停其他所有的工作线程,知道它收集结束。优点是简单和高效,缺点是不够灵活,适合运行在 Client 模式下的虚拟机。

   Serial Old 就是 Serial 在老生代的版本,也是适合运行在 Client 模式下的虚拟机。要是运行在Server 模式下, Serial Old 有以下的作用 :

  • 在 JDK 1.5以及之前的版本中与 Parallel Scavenage 收集器搭配使用
  • 作为CMS 收集器的后备预案,在并发收集发生Current Mode Failure 时使用

parNew 收集器

      它存在在新生代,就是Serial收集器的多版本。

Paranlle Scavenge 和 Paranlle Old 收集器

          Paranlle Scavenge 是一个新生代收集器,也是使用复制算法被称为“吞吐量优先”的收集器,它的特点有两点 :

  • 实现一个可控制的的吞吐量(Throughput)
  • GC 自适应的调节策略(GC Ergonomics)


          第一个特点,吞吐量就是运行用户代码时间占(运行用户代码时间+垃圾收集时间)的比值,可以通过参数调整吞吐量。Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)
          第二个特点,JVM根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

          Paranlle Old 是 Paranlle Scavenge 的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 组合的收集器。

CMS(Concurrent Mark Sweep) 收集器

       CMS收集器是一种以获取最短回收停顿时间为目标的基于“标记-清除”的收集器。

       CMS Collector也叫做low-latency collector,它是专门为老年代设计的。因为年轻代的stop-the-world时间不会太长,而对于老年代来说,虽然它的收集并不频繁,但是每次收集都可能会出现较长的停顿时间,尤其是在堆空间很大的时候。而CMS Collector的出世就是解决老年代停顿时间长的问题。解决这个问题它主要通过下面2个手段:

 

  1. 当老年代收集过后,CMS Collector并不会去compacting老年代,而是用空闲列表(free-lists)去管理被释放的空间。 这里需要注意的是 compacting 方式实际就是“标记-清除”,所以这里使用free-list 解决的是缩短了收集的时间,但是并没有解决“标记-收集”存在的问题。
  2. 它在mark-and-sweep阶段大部分的时候都是与我们自己的应用并发执行。

回收过程分四个步骤 :

  • 初始标记(CMS inital mark):标记与根集合节点直接关联的节点。时间非常短,需要STW
  • 并发标记(CMS concurrent mark):遍历之前标记到的关联节点,继续向下标记所有存活节点。时间较长。
  • 重新标记(CMS remark):重新遍历trace并发期间修改过的引用关系对象。时间介于初始标记与并发标记之间,通常不会很长。需要STW 所以可以知道并发标记的时间最长的并发标记,重新标志只是修改并发期间发生的变化的节点
  • 并发清理(CMS concurrent sweep):直接清除非存活对象,清理之后,将该线程占用的CPU切换给用户线程

缺点 :

  1. CMS 收集器对CPU 资源非常敏感
  2. CMS 收集器无法处理浮动垃圾(Floating Garbage),继而可能出现“Concurrent Mode Failure”失败而导致另一次
      Full GC 的产生。

CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾
   出现在标记过程之后,CMS 无法在当次收集中处理掉他们,这部分垃圾就是浮动垃圾。

 

      也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间用用户线程使用, 因此CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了在收集,CMS会预留一部分空间提供并发收集时的程序运行使用。要是预留的空间无法程序需要(浮动垃圾太多,需要回收的东西太多),就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案 : 临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

   3.    CMS 是基于“标记-清除”算法,空间碎片过多,可能出现无法分配大对象的情况,往往会出现老年代还有很大空间剩余,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC

G1 收集器

       可以独立运行,下面总结一下它的特点 :

  • 并行与并发 : 能充分利用多CPU来缩短 Stop-The-World 的时间
  • 分代收集 : 分代概念依旧得以保留
  • 空间整合 : 使用为区域定为“Region”的方法,保证了运行期间不会产生内存空间碎片,收集后能提供规整的可用内存,那么也就不会存在大对象无法分配的情况了。
  • 可预测的停顿

原理:

  • G1收集器将整个堆划分为多个大小相等的Region,每个Region 都有个Remembered Set 来避免全堆扫描的,G1 中每个Region 都有一个与之对应的Remembered Set
  • G1跟踪各个region里面的垃圾堆积的价值(回收后所获得的空间大小以及回收所需时间长短的经验值),在后台维护一张优先列表,每次根据允许的收集时间,优先回收价值最大的region,这种思路:在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存),并回收,做到尽可能的在有限的时间内获取尽可能高的收集效率。

收集器组合

Serial/Serial Old:

1_thumb

特点:

  • 年轻代Serial收集器采用单个GC线程实现"复制"算法(包括扫描、复制)
  • 年老代Serial Old收集器采用单个GC线程实现"标记-整理"算法
  • Serial与Serial Old都会暂停所有用户线程(即STW)

适用场合:

  • CPU核数<2,物理内存<2G的机器(简单来讲,单CPU,新生代空间较小且对STW时间要求不高的情况下使用)
  • -XX:UseSerialGC:强制使用该GC组合
  • -XX:PrintGCApplicationStoppedTime:查看STW时间
  • 由于它实现相对简单,没有线程相关的额外开销(主要指线程切换与同步),因此非常适合运行于客户端PC的小型应用程序,或者桌面应用程序(比如swing编写的用户界面程序),以及我们平时的开发、调试、测试等。

 

ParNew/Serial Old:

2_thumb

说明:

ParNew除了采用多GC线程来实现复制算法以外,其他都与Serial一样,但是此组合中的Serial Old又是一个单GC线程,所以该组合是一个比较尴尬的组合,在单CPU情况下没有Serial/Serial Old速度快(因为ParNew多线程需要切换),在多CPU情况下又没有之后的三种组合快(因为Serial Old是单GC线程),所以使用其实不多。

-XX:ParallelGCThreads:指定ParNew GC线程的数量,默认与CPU核数相同,该参数在于CMS GC组合时,也可能会用到


 

Parallel Scavenge/Parallel Old:

3_thumb

 

特点:

  1. 年轻代Parallel Scavenge收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
  2. 年老代Parallel Old收集器采用多个GC线程实现"标记-整理"算法
  3. Parallel Scavenge与Parallel Old都会暂停所有用户线程(即STW)

 

说明:

  1. 吞吐量:CPU运行代码时间/(CPU运行代码时间+GC时间)
  2. CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
  3. Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)

 

参数设置:

  1. -XX:+UseParallelOldGC:使用该GC组合
  2. -XX:GCTimeRatio:直接设置吞吐量大小,假设设为19,则允许的最大GC时间占总时间的1/(1 +19),默认值为99,即1/(1+99)
  3. -XX:MaxGCPauseMillis:最大GC停顿时间,该参数并非越小越好
  4. -XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC自适应调节策略),而我们需要设置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio两个参数就好(当然-Xms也指定上与-Xmx相同就好)

 

适用场合:

  1. 很多的CPU计算任务而用户交互任务较少的情况
  2. 不想自己去过多的关注GC参数,想让虚拟机自己进行调优工作
  3. 对吞吐量要求较高,或需要达到一定的量。

 

ParNew/CMS:

4_thumb

 

说明:

  1. 以上只是年老代CMS收集的过程,年轻代ParNew看"2.2、ParNew/Serial Old"就好
  2. CMS是多回收线程的,不要被上图误导,默认的线程数:(CPU数量+3)/4
  3. CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)

特点:

1.年轻代ParNew收集器采用多个GC线程实现"复制"算法(包括扫描、复制)

2.年老代CMS收集器采用多线程实现"标记-清除"算法,整个过程分4个步骤,上面已介绍了

3.初始标记重新标记都会暂停所有用户线程(即STW),但是时间较短;并发标记与并发清理时间较长,但是不需要STW

关于并发标记期间怎样记录发生变动的引用关系对象,在重新标记期间怎样扫描这些对象

 

参数设置:

  • -XX:+UseConcMarkSweepGC:使用该GC组合
  • -XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收
  • -XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW
  • -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理
  • -XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4

适用场合:

用于处理很多的交互任务的情况

方法区的回收一般使用CMS,配置两个参数:-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled

适用于一些需要长期运行且对相应时间有一定要求的后台程序


 

G1

5_thumb

 

说明:

  • 从上图来看,G1与CMS相比,仅在最后的"筛选回收"部分不同(CMS是并发清除),实际上G1回收器的整个堆内存的划分都与其他收集器不同。
  • CMS需要配合ParNew,G1可单独回收整个空间

运作流程:

  • 初始标记:标记出所有与根节点直接关联引用对象。需要STW
  • 并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。这里不需要STW,并且可以和客户一起并发执行,在此期间所有变化引用关系的对象,都会被记录在Remember Set Logs中
  • 最终标记:标记在并发标记期间,新产生的垃圾。需要STW
  • 筛选回收:根据用户指定的期望回收时间回收价值较大的对象(看"原理"第二条)。需要STW

优点:

  1. 停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
  2. 无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
  3. 筛选回收阶段:
  • 由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU,会降低吞吐量。
  • 由于STW,所以不会产生"浮动垃圾"(即CMS在并发清理阶段产生的无法回收的垃圾)

适用范围:

  • 追求STW短:若ParNew/CMS用的挺好,就用这个;若不符合,用G1
  • 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面没有优势

    

参考资料 :

原文地址:https://www.cnblogs.com/Benjious/p/10265144.html