JVM(三)-- 垃圾回收

257 阅读8分钟

一:风骚概述

JVM中垃圾收集算法主要有复制算法、标记--清除、标记--整理、分代收集,每种垃圾收集器可以说都是一种或多种垃圾收集算法的实现。堆空间分代、垃圾收集器、垃圾收集算法三者的关系可以用以下特点概括:

  • 堆空间:不同对象根据存活时间内存大小等特点分布在堆不同空间
  • 收集算法:不同堆空间对象回收率等存在明显差异,选用不同特点垃圾收集算法
  • 收集器:收集算法实例即垃圾收集器

二:回收算法

不同垃圾收集算法具备不同的特点,对于每块堆内存空间来讲都有比较符合存储对象特点的垃圾收集算法。主要介绍复制算法、标记 -- 清除、标记 -- 整理、分代收集算法

2.1 复制算法

在这里插入图片描述

  • 实现原理:将所有存活对象复制一份放入一块内存,然后将区域外所有内存回收
  • 适用区域:堆空间新生代对象朝生夕死,存活对象比例低
  • 算法缺点:需要使用一块内存区域作为复制对象存放区域,内存使用率低
  • 新生代优化:新生代朝生夕死存活率低,将内存默认分为8:1:1,默认使用1/10的S区域存放,避免大量内存浪费。同时使用分配担保策略减少OOM风险
2.2 标记 -- 清除算法

在这里插入图片描述

  • 实现原理:标记所有可回收对象将其进行回收
  • 适用区域:只有CMS收集器采用这个算法收集老年代
  • 算法缺点:容易造成内存碎片导致对象寻找不到合适内存从而诱发GC
  • 算法优点:实现简单,不需要挪动存活对象位置,在对象存活率较高场景下比较优秀
2.3 标记 -- 整理算法

在这里插入图片描述

  • 实现原理:标记可回收对象、将存活对象移动到连续内存区域、回收区域外内存
  • 适用区域:大部分老年代收集器采用算法
  • 算法缺点:在标记 -- 清除算法基础上增加对象挪动所以成本更高
  • 算法优点:相对于标记 -- 清除解决了碎片内存问题,相对于复制算法避免了预留区域的问题。一定注意该算法是先标记 --> 再移动整合 --> 最后收集这个时间操作轴
2.4 分代收集算法

这块内容留到G1垃圾收集器再专门介绍,简单理解就是收集器根据不同内存区域采用不同算法实现垃圾回收

三:垃圾收集器

在这里插入图片描述
根据不同垃圾收集算法实现的垃圾收集器具备不同特点,当然也就运用于不同内存区域。其中新生代垃圾收集器包括Serial、ParNew、Parallel Scavenge,老年代垃圾收集器Serial Old、Parallel Old、CMS以及分代收集器G1等。不同的垃圾收集器可以配合使用,根据不同的需求场景可以选择最合适的配合

3.1 Serial / Serial Old

在这里插入图片描述

  • Serial采用复制算法的新生代单线程垃圾收集器
  • Serial可配合老年代Serial Old、CMS进行垃圾收集
  • Serial是Client模式下默认垃圾收集器
  • Serial Old是Serial老年代实现采用标记 -- 整理算法,除了可以配合所有新生代垃圾收集器外还可以作为CMS的后备垃圾收集器
3.2 ParNew

在这里插入图片描述

  • 采用复制算法的新生代并行垃圾收集器,可看做为Serial的多线程版本
  • 可配合Serial Old、CMS进行垃圾回收工作
3.3 Parallel Scavenge / Parallel Old

在这里插入图片描述

  • 采用复制算法的新生代并行收集器,关注吞吐量。即垃圾收集与线程运行时间比例
  • 可配合Serial Old、Parallel Old实现垃圾回收,与Parallel Old组成一组吞吐量优先的垃圾收集器
  • 吞吐量设定参数-XX:MaxGCPauseMillis指定垃圾收集最大停顿毫秒数时间、参数-XX:GCTimeRatio指定GC线程与用户线程运行时间所占最大比例
  • 与ParNew最大的一个区别在于自适应策略,参数-XX:+UseAdaptiveSizePolicy打开。自适应解释为虚拟机控制新生代、老年代大小比例以及Eden、From、To区域比例
  • Parallel Old是Parallel Scavenge的老年代版本实现,采用标记 -- 整理算法。一组关注吞吐量的垃圾回收器
3.4 CMS

在这里插入图片描述

  • 垃圾收集划分为四个阶段,初始标记 --> 并发标记 --> 重新标记 --> 并发清除。初始标记仅仅标记GCRoot节点、重新标记仅仅修正并发标记阶段用户线程运行导致的对象关系变化。所以整体上来看CMS在这两个阶段虽然有STW但是影响不大,可以称得上并发收集器
  • CMS采用标记 -- 清除垃圾回收算法,导致回收后产生大量内存碎片,可能会导致频繁的发生GC
  • CMS是一款并发的垃圾收集器,也就是用户线程还在持续运行。垃圾收集不能等到内存空间100%使用再进行,需要预留部分内存供用户线程使用
  • 当并发进行的GC线程回收速度跟不上用户线程垃圾产生速度,这时就会提示Concurrent Mode Failure从而启动后备垃圾收集器Serial Old进行STW的垃圾回收
    在这里插入图片描述
3.5 G1

在这里插入图片描述

  • 首先需要明确G1垃圾收集器内存划分采用化整为零思想,一个个独立的Regin区域组成整个GC堆。新生代、老年代概念仅仅标识某些可以物理上不连续的Regin集合

    在这里插入图片描述

  • G1收集器还有一个比较重要的特征就是可预测停顿,这点与关注吞吐量的Parallel收集器类似。即在M时间段内可设置用户垃圾回收的时长不超过N,具体实现原理就是将所有Region的垃圾收集维护一个优先级表,GC时计算出N时长内效率最高的一些Region进行回收

  • 当某个对象位于Region1,引用该对象的对象位于Region2,那么在可达性算法分析时进行全堆扫描?G1中为每个Region维护一个Remembered Set,当其它引用对象对该某个对象对象产生引用关系时就会在引用对象的Remembered Set中记录,保证了引用关系标记的准确性

  • 除了初始标记与并发标记与CMS一致外,G1后两个阶段为最终标记与筛选回收。筛选回收也就是根据上面讲的优先级列表进行,该阶段会STW并行执行,且回收算法带有内存整合。最终标记就是将在并发标记阶段用户线程记录到Remembered Set logs的引用关系合并到Remembered Set中,修正标记结果

四:垃圾收集参数

垃圾收集器具备多种搭配,每种垃圾收集器又都有一些自己独特的配置。所以针对这两点,将从垃圾收集器选择参数以及收集器配置两方面讲解一些常用参数

4.1 Serial + SerialOld

这套垃圾收集器组合是Client模式下默认的垃圾收集器配置,若在Server模式下想强制指定该配置则使用参数-XX:+UseSerialGC即可。使用该组合垃圾收集器后新生代用DefNew 表示,老年代使用Tenured表示

在这里插入图片描述

4.2 ParNew + Serial Old

相对于Serial来讲ParNew基本可以说是多线程版本,同时它还是出了Serial之外唯一可以与CMS合作的新生代垃圾收集器。使用参数-XX:UseParNew强制指定,使用ParNew垃圾收集器后新生代使用ParNew标记

在这里插入图片描述

4.3 Parallel Scavenge + Parallel Old

关注吞吐量,JDK1.8默认的垃圾收集器组合。使用参数+XX:UseParallelGC强制指定,前面讲过相对于ParNew来讲,Parallel Scavenge还有最重要的特征就是堆内存分配自适应调节策略,使用参数-XX:UseAdaptiveSizePolicy参数打开。只需要设置最大最小堆,新生代老年代大小比例,新生代Eden、S区域比例,晋升老年代年龄等参数都会自动设置。这套组合的新生代用PSYoungGen、老年代用ParOldGen表示。当然吞吐量相关参数:

  • -XX:GCTimeRatio:0-100的整数,1 / (1 + GCTimeRatio) = 垃圾回收时间占比
  • -XX:MaxGCPauseMillis : GC使用最长时间,单位毫秒
    在这里插入图片描述
4.4 ParNew + CMS + Serial

使用参数-XX:+UseConcMarkSweepGC强制指定垃圾收集器,需要注意的是这里的Serial仅仅只是一个后备的垃圾收集器而已。当然使用CMS后老年代使用CMS表示,比较重要的一点就是CMS采用标记 -- 清除算法有可能会导致重复频繁的GC。为了控制这种情况,CMS提供参数-XX:CMSFullGCsBeforeCompaction指定多少次Full GC后进行一次内存整理

在这里插入图片描述

五:GC发生时间

讲完GC大部分相关内容后总得清楚什么时候发生GC这个操作呀,首先需要明确的是GC分为新生代的Minor GC 以及 老年代的Full GC。那么这两种GC到底在什么时候触发呢?

5.1 Minor GC

当新生代剩余连续内存不足以分配新生对象,老年代剩余空间满足分配担保策略需求的时候会执行Minor GC,如若不然执行Full GC

在这里插入图片描述

5.2 Full GC

除了上述说的因为Minor GC引发的Full GC之外,还有什么情况会导致Full GC的发生?如下所示:

  1. 显示调用System.gc(),一般不会做这么蠢的操作
  2. 大对象直接晋入老年代导致的老年代内存不足
  3. 方法区内存不足导致需要进行类卸载的GC