三、垃圾回收

3.1、如何判断对象可以回收

1、引用计数法

  • 定义

此种算法会在每一个对象上记录这个对象被引用的次数,只要有任何一个对象引用了次对象,这个对象的计数器就+1,取消对这个对象的引用时,计数器就-1,在分配对象时会将计数器的值置为1

任何一个时刻,如果该对象的计数器为0,那么这个对象就是可以回收的。

  • 缺点
  1. 计数器值增减频繁
  2. 实现繁琐,更新引用时很容易导致内存泄露。
  3. 循环引用无法回收(最重要的缺点),两个已经失去作用、但互相引用的对象无法被引用计数法判断为可回收垃圾。

image-20210521172324702

  • 注意:JVM 没有使用引用计数法

2、可达性分析方法

  • 定义

这个算法的基本思想是通过一系列称为 “GC Roots“(肯定不能被当为垃圾回收的对象) 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象可以被当成垃圾进行回收。

JVM 中的垃圾回收器采用可达性分析算法来探索所有存活的对象。

  • 哪些对象可以作为 GC Root
  1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

  2. 方法区中的类静态属性引用的对象和常量引用的对象。

  3. 本地方法栈中JNI(Native方法)引用的对象。

  4. 正在加锁的对象

3、五种引用

image-20210521193931546

image-20210521190911142

Java 中常见的五种引用分别为:强引用、弱引用、虚引用、软引用和终结器引用

  • 强引用:不回收

如果一个对象具有强引用,那就类似于必不可少的物品,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。

Java 中绝大部分引用都是强引用。

  • 软引用:内存不足即回收

软引用是用来描述一些有用但并不是必需的对象,在Java中用 java.lang.ref.SoftReference 类来表示。

对于只有软引用的对象来说:当系统内存充足时它不会被回收,当系统内存不足时它才会被回收。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

  • 弱引用:发现即回收

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短。

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存。

ThreadLocal 中的 ThreadLocalMapEntry 就继承了 WeakReference

image-20210521192342894

  • 虚引用:对象回收跟踪

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。

虚引用,顾名思义,就是形同虚设,与其他几种引用都不太一样,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

虚引用需要java.lang.ref.PhantomReference 来实现。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(RefenenceQueue)联合使用。

虚引用的主要作用是跟踪对象垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。

  • 终结器引用

它用以实现对象的 finalize 方法,也可以称为终结器引用。无需手动编码, 其内部配合引用队列使用。

3.2、垃圾回收算法 – 标记清除

1、定义

标记清除算法是一种分两阶段对对象进行垃圾回收的算法。

第一阶段:标记。从根节点(GC Root)出发遍历对象,对访问过的对象打上标记(一般是在对象的 Header 中记录),证明该节点为可达,并非可以回收的垃圾节点

第二阶段:清除。对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

image-20210521201558262

2、缺点

  • 回收后会产生大量不连续的内存空间,即内存碎片。

由于Java在分配内存时通常是按连续内存分配,那么当碎片空间不足以分配给新的对象时,就造成了内存浪费。

  • 进行垃圾回收时,应用需要挂起
  • 标记和清除的效率不高,尤其是要扫描的对象比较多的时候

3、优点

  • 可以解决循环引用的问题
  • 必要时才回收(内存不足时)

3.3、垃圾回收算法 – 标记整理

image-20210521204010481

1、定义

标记整理算法和标记清除算法一样分为两个阶段,即先标记后整理。由于标记清除算法的一个缺点就是会产生大量内存碎片,而标记整理算法会对内存空间进行一次整理,解决内存碎片化问题。

  • 标记。从根节点(GC Root)出发遍历对象,对访问过的对象打上标记(一般是在对象的 Header 中记录),证明该节点为可达,并非可以回收的垃圾节点
  • 整理。在遍历结束后, 对于标记过的对象,把它们从内存开始的区域按顺序依次摆好,整整齐齐的, 中间没有任何的缝隙。在摆放完最后一个标记过的对象后, 把之后的内存区域直接回收掉. (这里最耗时的步骤是,当你移动一个对象的内存位置时,你需要让所有之前依赖这个对象的对象更新一下引用地址信息,这样才不会在移动之后出错.)

2、优点

  • 消除了标记清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

3、缺点

  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  • 移动过程中,需要全程暂停用户应用程序。

3.4、垃圾回收算法 – 复制

1、定义

复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的

当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址

此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

  • 垃圾清理前(1、4 为垃圾)

image-20210521231513342

  • 将所有存活对象复制到原来的空闲区间中,并按照内存地址排序,更新引用,然后清除原来活动区间(现空闲区间)中的所有垃圾对象(1、4)

image-20210521231610100

2、优点

  • 不产生内存碎片问题,能保持对象的完整性。
  • 可实现高速分配

GC 复制算法不使用空闲链表。这是因为分块是一个连续的内存空间。比起 GC 标记 - 清除算法和引用计数法等使用空闲链表的分配,GC 复制算法明显快得多。

3、缺点

  • 复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视

如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。

  • 堆使用效率低下,会浪费 50% 的内存

GC 复制算法把堆二等分,通常只能利用其中的一半来安排对象。也就是说,只有一半 堆能被使用。相比其他能使用整个堆的 GC 算法而言,可以说这是 GC 复制算法的一个重大的缺陷。

通过搭配使用 GC 复制算法和 GC 标记 - 清除算法可以改善这个缺点

复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费

3.5、三种垃圾回收算法对比

1、内存整齐度

复制算法 = 标记整理算法 > 标记清理算法

2、内存利用率

标记整理算法 = 复制算法 > 标记清理算法

3.6、分代垃圾回收

1、分代说明

堆内存是JAVA虚拟机所管理的内存最大的一块,Java堆被所有线程共享,几乎所有的对象实例都是在堆中分配内存,因此Java的堆是垃圾回收的主要区域

JVM的内存分代讲的就是堆内存的分代,为了更加高效的回收垃圾,将内存划分为了多个generation(代)。

JVM堆可以划分为新生代、老年代、永久代(JDK1.7),在JDK1.8中,永久代被元空间(Metaspace)所代替,并且元空间已经不在堆中了。

2、永久代和元数据的区别

永久代是 HotSpot 虚拟机特有的概念,并且在JDK1.8之后,永久代就彻底消失了。

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,并且永久代必须指定大小限制,因此就会导致性能问题和内存溢出的问题。永久代会给GC带来不必要的复杂性。

元空间的本质和永久代类似,但是元空间并不在堆中,而是直接使用了本地内存,元数据可以设置限制,也可以不设置,它的大小仅受本地内存限制。

3、新生代和老年代

新生代和老年代是垃圾回收最主要的区域。

一般将更有价值,需要长时间存活的对象放在老年代中,用完则丢弃的对象放在新生代中。

老年代的垃圾回收触发频率较低,新生代频繁触发垃圾回收

image-20210522131113329

新生代和老年代都在堆内存中,新生代和老年代所占的默认比例为1 : 2,其中新生代又由一个伊甸(Eden)区和两个幸存者(Survivor)区组成,三个区的默认比例为8:1:1。

  • 新生代

YoungGC 对应于新生代,第一次YGC只回收 eden 区域,回收后大多数(百分之九十八左右)的对象会被回收,活着的对象通过复制算法进入Survivor0(后续用S0和S1代替)。再次YGC后eden+S0中活着的对象进入S1。再次YGCeden+S1中活着的对象进入到S0。依次循环。

在将 eden 区与其中一个survivor区作为 From 区时,需要将另外一个survivor区作为 To 区,即保证 To 区为空。

  • 当一个对象的年龄(经历的YGC次数)足够时(传统的垃圾回收器一般是15,CMS垃圾回收器是6),进入老年代

  • 如果遇到一个对象S区装不下,则直接进入老年代。

  • 老年代

老年代的垃圾回收或称叫做 FullGC,当老年代空间不足时,就会触发 FullGC;另外,如果元空间区域的内存达到了所设定的阈值-XX:MetaspaceSize=,也会触发FullGC

FullGC 采用的是标记整理算法,这个算法的效率是比较低的,因为它要标记出或者的对象,然后移到内存的一侧,最后再清空区域外的内存。这个过程会十分消耗时间。

因此优化 JVM 最重要的一点就是优化 FullGC,尽可能的不要执行 FullGC

4、对象转移过程

  • 当我们创建一个新对象后,这个新对象会默认占用新生代中伊甸(Eden)区的一块空间
  • 当伊甸区空间不够时,此时会触发一次 Minor GC(Young GC),第一次 Minor GC 只针对伊甸区,并将第一块幸存者(survivor0)区作为复制算法的 To 区,然后将存活下来进入幸存区的对象的寿命 + 1。
  • 此时由于伊甸区已经被清空,所以后面新创建的对象可以继续存放在伊甸区中,在后面的复制算法中,会将伊甸区和上面的第一块幸存者(survivor0)区作为 From 区,然后将另外一块幸存者(survivor1)区作为 To 区,此时需要对幸存的对象的寿命 + 1
  • 新生代中的对象不会永远呆在新生代中,当新生代中对象的寿命超过一个阈值(15)时,这个对象会被转移到老年代。GC分代年龄存储在对象的 header 中
  • 如果遇到一个对象 Survivor 区装不下,则直接进入老年代。
  • 当新生代空间实在不足或老年代空间不足时,会使用 Full GC 对新生代与老年代进行一次力度较大的垃圾回收。
  • Minor GC 会引发 stop the world,即暂停其他用户线程,直到垃圾回收线程完成工作后继续运行其他用户线程,Minor GC 使用的时间较短
  • 对象寿命的阈值是15,超过这个阈值,新生代对象会被转移到老年代中,这是由于对象头中,寿命占 4 bit,而4 bit 最大的值即为15,不同垃圾回收器中寿命阈值不同。
  • Full GC 也会引发 stop the world,但占用的时间会更长。

5、相关 VM 参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx-XX:MaxHeapSize=size
新生代大小-Xmn(-XX:NewSize=size + -XX:MaxNewSize=size)
幸存者比例(动态)-XX:InitialSurvivorRatio=ratio-XX:+UseAdaptiveSizePolicy
幸存者比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC 详情-XX:+PrintGCDetails -verbose:gc
Full GCMinor GC-XX:+ScavengeBeforeFullGC

6、分析以下参数和日志

在参数中,我们指定堆初始大小和堆最大大小为 20M,新生代大小为 10M ,所以老年代大小为 10M (20 - 10)

image-20210522143436418

  • 在截图中,显示了堆和元空间的数据

image-20210522143718997

在新生代中,内存空间被划分为3块,其中伊甸区占 4/5 ,其余两块幸存区各占 1/10 ,由于一块幸存者区要作为复制算法的 To 区,所以 10M 内存中有 1M 必须为空,故可用内存只有 9M.

total 为可用的内存大小,used 表示已经使用的大小

image-20210522144237324

从上面日志中可以看到新生代中内存划分和分配比例。

image-20210522144500450

上面日志打印了老年代的内存占用信息,由于堆初始大小、堆最大大小和新生代内存大小都已经被指定,所以老年代内存大小也被指定。

7、大对象直接晋升至老年代

对于无法放入新生代,但可以放入老年代的大对象来说,JVM 会直接将该对象放入老年代,不会触发 GC

  • 放置大对象前

image-20210522143436418

  • 往堆中直接塞一个 8M 的大对象,查看日志

image-20210522145752735

可以看到,大对象直接被塞进了老年代,同时没有触发 GC

  • 一个线程的OOM不会导致进程失效

3.7、垃圾回收器

1、串行(Serial)

  • 简介

Serial 是一类用于新生代单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停

适用于堆内存较小的个人电脑

image-20210522153952355

从上图可知当应用程序进行到一个安全的节点的时候,所有的线程全都暂停,等到GC完成后,应用程序线程继续执行。

  • 优点

简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个 cpu 来说没有了上下文之间的的切换,效率比较高。

  • 缺点

会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。

  • 开启命令

其中 Serial 工作在新生代,使用的算法是复制算法

SerialOld 工作在老年代,使用的是标记整理算法

1
-XX:+UserSerialGC = Serial + SerialOld

2、吞吐量优先

使用于多线程、堆内存较大、拥有多核 CPU 的环境(服务器)

  • 简介

Parallel Scavenge 是一款用于新生代的多线程收集器,采用复制算法。与 ParNew 的不同之处在于 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量,而 ParNew 收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

image-20210522152437296

  • 优点

追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。

  • 开启命令

JDK 8 默认开启,ParallelOldGC 工作在老年代,使用 标记整理算法

1
-XX:+UseParallelGC ~ -XX:++UseParallelOldGC

3、响应时间优先

使用于多线程、堆内存较大、拥有多核 CPU 的环境(服务器)

  • 简介

ParNew 收集器其实就是Serial的一个多线程版本,其在单核 cpu 上的表现并不会比 Serail 收集器更好,在多核机器上,其默认开启的收集线程数与 cpu 数量相等。

image-20210522152620792

  • 优点

随着 cpu 的有效利用,对于GC时系统资源的有效利用有好处。

  • 缺点

会在用户不知道的情况下停止所有工作线程

3.8、垃圾回收器 – G1

这块垃圾回收器的特点是 区域化分代式

1、定义

G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成为 HotSpot 重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器

G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

G1 是一个并行回收器,它将堆内存分割为很多个不相关的区域(Region),使用不同的 Region 来表示 Eden 、 幸存者 0 区 、 幸存者 1 区和老年代等。

  • 2004 论文发布
  • 2009 JDK 6 体验
  • 2012 JDK 7 官方支持
  • 2017 JDK 9 默认

官方给 G1 设定的目标是在低延迟可控的情况下获得尽可能高的吞吐量

G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾回收,G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个 优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

2、使用场景和相关 JVM 参数

  • 同时注重吞吐量和低延迟,默认的暂停目标是 200 ms

  • 适合超大堆内存,G1 会将堆划分为多个大小相等的 Region

  • 整体上是标记 + 整理算法,两个区域间是复制算法

  • 开启方式

1
-XX:+UseG1GC
  • 指定 G1 区域的大小
1
-XX:G1HeapRegionSize=size

3、G1 回收器的特点

与其他的 GC 收集器相比, G1 使用了全新的分区算法,其特点如下

  • 并发与并行
  1. 并行性

G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核 CPU 的计算能力。此时用户线程需要进行 STW

  1. 并发性:

G1 拥有与应用程序交替执行的能力,部分工作可以与应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。

  • 分代收集
  1. 从分代上看,G1 依然属于分代型垃圾回收器,它会区分年轻代与老年代,年轻代依然有 Eden 区和 Survivor 区。但从堆的结构上看,它不要求整个 Eden 区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量

  2. 堆空间划分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。

  3. G1 同时兼顾老年代和年轻代

  • 空间整合

G1 的垃圾回收以 Region 作为基本单位,Region 之间是复制算法,但整体上实际可以看作 标记 - 压缩 算法,这两种算法都可以避免内存碎片

这种特点有利于程序长时间运行,分配大对象时不会因为无法找到连续空间而提前触发下一次 GC ,尤其是当 Java 堆非常大时,G1 的优势更加明显

4、分区 Region

化整为零,将之前物理连续的年轻代、老年代打散开来,分为一块块大小相同,但在物理上不连续的分区,对于一块 Region,它扮演的角色是单一的,不能半块是 Eden、半块是老年代

但 Region 的角色是可以改变的,当一块 Eden 区被清理干净后,它可以变为其他区域。

image-20210825214259444

  • humongous区

主要用于存储大对象,如果对象超过 1.5 个 Region,那么就放到 H 区

如果一个 H 区无法装入大对象,那么 G1 会寻找连续的 H 区来存储,为了能找到连续的 H 区,有时候不得不启动 FULL GC。

G1 的大多数行为都把 H 区看为老年代的一部分。

5、G1 垃圾回收阶段

image-20210522192817910

G1 垃圾回收器的回收工作可以分为三个阶段,分别为

  • Young Collection :对新生代的垃圾进行收集
  • Young Collection + Concurrent Mark: 对新生代垃圾进行收集,同时对老年代添加并发标记
  • Mixed Collection :混合垃圾收集(对新生代、新生区、老年代都进行一次规模较大的垃圾收集)

以上三个过程循环进行

如果需要,单线程、独占式、高强度的 Full GC 依然会存在,它针对 GC 的评估失败提供了一种失败保护机制,即强力回收

6、Young Collection

会产生 Stop The World ,阻塞其他用户线程

注意,只有当 Eden 区空间不够的时候才会触发 Young GC ,幸存区空间不够时不触发

  • 新创建的对象放入伊甸(Eden)区中,在新生区的伊甸区被占满后,此时会触发一次 Young Collection

image-20210522193725471

  • Young Collection 会将伊甸区中的存活对象拷贝到幸存区中。

image-20210522194038070

  • 在幸存区空间不足或者幸存区对象寿命达到阈值后,此时幸存区中的一部分对象会被放入老年代,一部分会被当成垃圾回收,另一部分会放入其他的幸存区中。

image-20210522201635379

7、Young Collection + CM

  • Young GC 时会进行 GC Root 的初始标记
  • 老年待占用堆空间比例达到阈值时,进行并发标记(不会 STW ),由下面的 JVM 参数决定
1
-XX:InitiatingHeapOccupancyPercent=percent(默认为 45%)

image-20210522204057591

8、Mixed Collection

会对 E 、 S 、O 进行全部垃圾回收

  • 最终标记(Remark)会产生 STW
  • 拷贝存活(Evacuation)会产生 STW

image-20210522204500487

由于我们会指定一个 GC 最大暂停时间,在这个事件内,G1 可能无法对所有老年代垃圾进行回收,所以它会有选择地回收一部分老年代的垃圾(回收价值最大的垃圾)进行回收

优先回收垃圾最多的区域

9、Full GC 和 Minor(Young) GC

  • Serial GC

    • 新生代内存不足发生的垃圾收集 - Minor GC
    • 老年代内存不足发生的垃圾收集 - Full GC
  • Parallel GC

    • 新生代内存不足发生的垃圾收集 - Minor GC
    • 老年代内存不足发生的垃圾收集 - Full GC
  • CMS

    • 新生代内存不足发生的垃圾收集 - Minor GC
    • 老年代内存不足
  • G1

    • 新生代内存不足发生的垃圾收集 - Minor GC
    • 老年代内存不足

    在 G1 进行并发标记、混合收集(且垃圾清除的速度大于垃圾产生速度)时,不直接称为 Full GC,此时仍然处于并发垃圾收集的阶段;

    只有垃圾回收的速度更不上垃圾产生的速度时,这个时候并发收集失败,这个时候退化为一个串行(Serial)收集,此时称为 Full GC

10、JDK 8 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 CPU 时间,新生代回收时间略微增加

开启参数

1
-XX:+UseStringDeduplication # 默认打开
1
2
String s1 = new String("hello"); // char[] {'h','e','l','l','o'}
String s2 = new String("hello"); // char[] {'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1 并发检查是否有字符串重复
  • 如果它们值一样,让他们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 字符串去重关注的是 char[]
    • JVM 内部,使用了不同的字符串表

11、JDK 8 并发标记类卸载

所有对象都经过并发标记后,就能直到哪些类不再被使用,当一个类加载器的所有类都不再使用后,则卸载它所加载的所有类

1
-XX:+ClassUnloadingWithConcurrentMark #默认启用

12、巨型对象

  • 一个对象大于 Region 的一半时,称为巨型对象。
  • G1 不会对巨型对象进行拷贝,回收时优先考虑巨型对象。
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时被处理掉。

巨型对象存放在 H 区中。

12、Remembered Set

与 CMS 相比,G1 还需要额外消耗一部分内存(大约是 10 % - 20 %)用于自身运行,这一部分额外消耗的内存中就包含了 Remembered Set

  • Remembered Set 的出现是为了解决一个对象被不同区域引用的问题

Region 不可能是独立的,一个 Region 中的对象可能被任意其他 Region 引用,如果这个对象被其他 Region 引用,那么在判断此对象是否存活时,我们可能需要对其他 Region 进行扫描(甚至是整个堆)

如果在回收新生代垃圾时,这个 Region 中的对象被老年代 Region 所引用,此时我们将必须扫描老年代 Region ,此时会降低 Minor GC 的效率

  • G1 使用了 Remembered Set避免进行全局扫描

对于每个 Region 都有对应的一个 Remembered Set

  • 在每次进行引用类型数据的写操作时,都会产生一个 Write Barrier 来暂停中断操作

然后检查将要写入的引用指向的对象是否和该引用类型对象处于不同的 Region **,如果不同,那么将信息记录到 **Remembered Set

  • 在进行垃圾回收时, Remembered Set 的存在就可以保证不进行全局扫描,也不会有遗漏

3.9、三色标记算法

1、简介

三色标记法是一种垃圾回收法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

2、三色说明

  • 白色

该对象从未被标记过(对象垃圾)

  • 灰色

该对象已经被标记过,但该对象下的属性没有完全标记完(GC需要从此对象中去寻找垃圾)

  • 黑色

该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)

image-20210821162935333

3、算法流程

从 GC Root 对象开始沿着它们的对象向下查找,使用黑灰白的规则,标记除所有与 GC Root 相连接的对象,在第一遍扫描结束后,一般需要进行一次短暂的 STW ,再次进行扫描,此时由于黑色对象的属性都已经扫描标记完成,所以只需要对灰色对象的属性进行扫描标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多), 此时程序继续执行,GC 线程扫描所有的内存,找出扫描之后依旧被标记为白色的对象(垃圾)清除。

  • 首先创建三个集合,即白、灰、黑
  • 将所有对象全部放入白色集合中
  • 从 GC Root 开始遍历所有对象,将遍历到的对象从白色集合放入灰色集合。
  • 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合,重复本次操作直到灰色集合为空
  • 通过write-barrier检测对象有变化,重复以上操作
  • 收集所有白色对象(垃圾)

4、存在问题

  • 浮动垃圾

在并发标记的过程中,如果一个对象已经被标记为黑色或者灰色,那么即使其在标记期间变为垃圾,由于不会再对黑色标记过的对象重新扫描,所以这个垃圾对象不会被发现,也自然不会清除,浮动垃圾对系统的影响不大,将这块垃圾留给下一次 GC 进行处理即可

  • 对象漏标(需要的对象被回收)

并发标记过程中,一个业务线程将一个未被扫描过的白色对象断开引用使其称为垃圾,同时一个黑色对象引用了该对象;此时由于不会再对黑色对象及其属性进行扫描,所以这个本不该成为垃圾的对象没有被标记,导致需要的对象被 GC 回收,漏标问题可能导致系统需要的对象被当成垃圾回收,所以可能会导致系统出现问题。

3.10、垃圾回收器 - CMS

这款垃圾收集器的特点是 低延迟

1、简介

  • 在 JDK 5 时期,HotSpot 推出了一款在强交互应用中几乎可认为是有划时代意义的垃圾回收器,即 CMS (Concurrent-Mark-Sweep),这款垃圾回收器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾回收线程与用户线程同时工作
  • CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。一般来说,停顿时间越短,就越适合与用户交互的程序,良好的响应速度可以提升用户体验

目前很大一部分的 Java 应用集中在互联网网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验

而 CMS 垃圾收集器就非常适合这类应用的需求。

  • CMS 垃圾收集器作用于老年代,采用标记 - 清除算法回收垃圾,也会 STW

2、工作流程

image-20210824221654007

在进行初始标记时,用户线程会 STW ,不过这个时间非常短,在进行重新标记时也会 STW

CMS 的垃圾回收过程主要分为四个阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

  • 初始标记(Initial-Mark)阶段

在这个阶段中,程序中所有的工作线程都会因为 STW 机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记处 GC Roots 能直接关联到的对象

一旦标记完成之后就会立即恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快

  • 并发标记(Concurrent-Mark)阶段

从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长不需要暂停用户线程,可以与垃圾收集线程一起并发运行。

  • 重新标记(Remark)阶段

由于在并发标记过程中,线程的工作线程会和垃圾收集线程同时或交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分的对象的标记记录,这个阶段的停顿时间通常比初始标记的停顿时间长,但远短于并发标记消耗的时间

  • 并发清除(Concurrent-Sweep)阶段

此阶段清理删除掉标记阶段判定的已经死亡的对象,释放内存空间,由于不需要对存活的对象进行整理移动,所以这个阶段也是可以与用户线程同时并发的。

清除后会产生内存碎片的问题。

3、特点

目前所有的垃圾回收器都无法做到完全不 STW ,只能做到尽量减少 STW 的时间

  • 低延迟(低停顿)

由于最耗费时间的并发标记阶段与并发清除阶段都不需要暂停用户线程,所以整体的回收是低停顿的

  • 在堆内存使用率达到一定阈值时就要开始回收垃圾

由于在并发标记、并发清除两个最耗时的阶段没有暂停用户线程,所以我们应该保证程序的用户线程拥有足够的内存可用(想象一下,如果并发标记过程中用户现场跑一半发现内存不够了该怎么办?)。

因此,CMS 收集器不能像其他收集器那样等到老年代几乎被填满了再进行回收,而是当堆内存的内存达到某一阈值时,就要开始进行垃圾回收,以确保应用程序在 CMS 工作过程总依然有足够空间支持应用程序运行

如果 CMS 运行期间预留的内存无法满足程序需要,那么就会出现一次 Concurrent Mode Failure 失败,这时虚拟机会启动后备预案,即临时启用 Serial Old 收集器来重新进行老年代的垃圾回收,这样停顿时间就很长了。

4、为什么 CMS 不使用标记压缩算法?

这是因为 CMS 在并发清除阶段时,没有进行 STW ,而是与用户线程并发运行,在进行垃圾回收时,用户线程还在工作,而标记压缩算法需要对存活对象进行压缩整合,这涉及到空间的重新分配,所以为了保证清除过程中用户线程可以继续执行,我们需要保证它的资源(对象空间)不受影响,所以不能使用标记压缩算法

5、优点

  • 低延迟
  • 并发收集垃圾

6、缺点

  • 会产生内存碎片

CMS 采用的标记清除算法可能**会降低内存的利用率,造成内存碎片。在无法分配大对象的情况下,可能会提前触发 Full GC **(可能老年代的空间总和远远大于要分配的大对象,但是由于内存碎片的存在,导致没有一块连续的空间能够容纳这个大对象,此时触发 Full GC)

  • CMS 收集器对 CPU 资源非常敏感

在并发阶段,它虽然不会导致用户线程停顿,但由于垃圾回收线程占用了一部分线程资源,所以导致应用程序对外界的总吞吐降低。

  • CMS 收集器无法处理浮动垃圾

7、CMS 在后续 JDK 中的变化

  • 在 JDK 9 中, CMS 被标记为 Deprecate
  • 在 JDK 14 中, CMS 被彻底删除