深入理解JVM—垃圾回收器(Grabage Collector)进阶篇

       在上一篇博客深入理解JVM—垃圾回收器(Grabage Collector)基础篇我们了解了Java判定一个对象是否为垃圾的两种算法,以及3种垃圾回收算法。如果说收集算法室是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。接下来我们就一起来了解一下常见的垃圾收集器。

1、垃圾收集器

在G1垃圾出现之前,JVM是对堆内存进行分代管理的,分为新生代和老年代,按照这样的划分,不同分区的回收器个有3种:

1.1 新生代垃圾回收器

(1)Serial
    Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3之前)是HotSpot虚拟机新生代收集器的唯一选择,时至今日,垃圾收集器的不断改进,不断出新,但是Serial依然在我们的垃圾收集器的选项里面。

Serial收集器的特点

比较 概述
垃圾回收算法 复制算法
使用范围 新生代
应用 Client模式下的默认新生代收集器
优点 简单高效,在单CPU的环境下,Serial收集器由于没有线程交换的开销,有着很高的垃圾收集效率。
缺点 垃圾收集过程中需要长时间的暂停所有用户线程,官方把这种现象称为Stop The World,对于现在的Java应用长时间的Stop The World势必会影响用户的体验。

开启Serial收集器:
使用Serial收集器需要使用一个JVM参数:-XX:+UserSerialGC
启用Serial收集器

(2)ParNew
    ParNew收集器是Serial收集器的多线层版本,除了使用多线程进行垃圾收集之外,其余行为与Serial完全一致。在实现上,两者也共用了相当多的代码。他可以配合CMS以及Serial Old这两个老年代收集器工作。ParNew默认开启的垃圾收集线程数与CPU的数量相同,可以使用-XX:ParllelGCThreads参数来限制垃圾收集的线程数。

ParNew收集器的特点

比较 概述
垃圾回收算法 复制算法
使用范围 新年代
应用 运行在Server模式下的虚拟机中首选的新生代收集器
优点 在多CPU时,比Serial效率高
缺点 收集过程暂停所有应用程序线程,单CPU时比Serial效率差。

(3)Parallel Scavenge(PS)
    Parallel Scavenge收集器也是一款新生代收集器,它同样是基于复制算法实现的收集器,它是能够并行收集的多线程收集器。Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

  • 吞吐量 = 运行用户代码时间/运行用户代码时间 + 运行垃圾收集器时间。
  • 比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
  • 若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。

Parallel Scavenge收集器的特点

比较 概述
垃圾回收算法 复制算法
使用范围 新生代
应用 高吞吐量为目标,即减少垃圾收集时间(就是每次垃圾收集时间短,但是收集次数多),让用户代码获得更长的运行时间; 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;就是说可以计算完后进行一次长时间的GC。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序
优点 多线程、可以控制吞吐量(-XX:MaxGCPauseMillis 最大垃圾收集停顿时间,-XX:GCTimeRatio 吞吐量大小)、GC自适应调节策略(-XX:+UserAdaptiveSizePolicy)
缺点 垃圾收集过程中需要长时间的暂停所有用户线程,官方把这种现象称为Stop The World,对于现在的Java应用长时间的Stop The World势必会影响用户的体验。

使用Parallel Scavenge收集器 -XX:+UseParallelGC,在启用Parallel Scavenge收集器后会自动开启Parallel Old收集器
启用Parallel Scavenge收集器

1.2 老年代垃圾回收器

(1)Serial Old收集器
    Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要也是供客户端模式下的HotSpot虚拟机使用。

Serial Old收集器的特点

比较 概述
垃圾回收算法 标记-整理算法
使用范围 老年代
应用 (1)在JDK1.5之前与Parallel Scavenge收集器搭配使用
(2)作为CMS收集器在发生Concurrent Mode Failure时的后备收集器
优点 简单高效,在单CPU的环境下,Serial收集器由于没有线程交换的开销,有着很高的垃圾收集效率。
缺点 垃圾收集过程中需要长时间的暂停所有用户线程,官方把这种现象称为Stop The World,对于现在的Java应用长时间的Stop The World势必会影响用户的体验。

(2)ParallelOld(PO)
       ScavengeParallel Old是Parallel Scavenge收集器的老年代版本,在JDK1.6开启提供服务,出现它的目的就是和Parallel Scavenge配合工作,它们可以在注重吞吐流以及CPU敏感的场合下使用。(在没有Parallel Old之前,Parallel Scavenge只能和Serial Old配合工作,Serial Old是一个单线程的收集器,无法发挥服务器下多CPU的性能,因此Parallel Scavenge的性能自然大大折扣)

Parallel Old收集器的特点

比较 概述
垃圾回收算法 标记-整理算法
使用范围 老年代
应用 配合Parllel Scavenge应用于在注重吞吐率以及CPU资源的敏感场合
优点 多线程收集
缺点 垃圾收集过程中需要长时间的暂停所有用户线程,官方把这种现象称为Stop The World,对于现在的Java应用长时间的Stop The World势必会影响用户的体验。

(3)CMS(Concurrent Mark Sweep)
    CMS(Concurrent Mark-Sweep)是一款并发的、使用标记-清除算法的垃圾回收器。对于要求服务器响应速度的应用,这种垃圾回收器非常适合。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。

CMS收集器的特点

比较 概述
垃圾回收算法 标记-清除算法
使用范围 老年代
应用 应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU以及系统在运行的时候有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS
优点 并发收集、低停顿
缺点 (1)CMS对CPU资源十分敏感。CMS默认启动的垃圾收集线程数量是**(CPU数量+3)/4**,显然随着CPU的增多处理垃圾的线程也比增多,那么对用户线程的影响就可能变大。
(2)CMS无法处理浮动垃圾,可能导致Concurren Mode Failure而退化为Serial Old单线程垃圾收集器从而引发另一次Full GC,造成程序停顿时间变的更长。
(3)CMS使用的垃圾回收算法导致了它会产生内存碎片化的问题,官方为了解决这个问题提供了一个参数:-XX:+UseCMSCompactAtFullCollection这个参数默认时开启的, 另外为了解决在进行内存整理的时候过长时间的停顿,官方提供了一个参数:-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩Full GC后跟着来一次带压缩的。默认是0,表示每次Full GC后都压进行碎片整理。

开启CMS垃圾回收器
使用CMS收集器只需要一条JVM参数: -XX:+UseConcMarkSweepGC
启用CMS收集器

CMS的工作阶段
从上面的图中我们可以看到,CMS来及回收器的工作分为4个阶段:

  • 初始标记:会触发STW,并且是单个GC线程工作,主要是标记一下GC ROOts能直接关联到的对象,速度很快;在JVM调优的时为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。

Java中GC Roots包含一下几种:
(1)虚拟机桟中引用的对象
(2)方法区中类静态属性引用的变量
(3)方法区中常量引用的对象
(4)本地方法栈中JNI的引用的对象

  • 并发标记:这一过程是GC线程和Java用户线程并发执行的,遍历初始标记阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。

  • 重新标记:由于之前并发标记阶段是GC线程和用户线程同时运行的,因此会导致一部分GC Roots引用发生改变了,这一次标记就是用来修正这些改变的。这一过程只有GC线程在执行,因此会产生STW

  • 并发清除:这一阶段GC线程和用户线程并发执行,主要是清除那些没有标记的对象并且回收空间;由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

2、G1(Garbage-First)垃圾收集器

G1
       G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命(在JDK9中已经被设置为默认垃圾回收器),也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:

  1. G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  2. G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
  3. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
  4. G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
使用场景
  • 同时注重吞吐量和低延时,默认的暂停目标是200ms
  • 对于超大堆内存,,会将堆划分为多个大小相等的Region
  • 整体上采用标记-整理算法,两个Region(区域)之间采用复制算法
相关JVM参数:
  • -XX:+UserG1GC 开启G1收集器
  • -XX:G1HeapRegionSize=size 设置每个Region的大小
  • -XX:MaxGCPauseMillis=time 设置GC暂停的时间

留言区

还能输入500个字符