梁恒嘉的技术专栏 全栈之路

JVM垃圾回收

2022-10-05
NanKe
JVM

主要记录垃圾回收的知识,包括在Java程序中垃圾的定义,垃圾回收机制的运行,垃圾回收中各个阶段回收算法,引用技术算法,根可达算法,对象的finalization机制,标记复制算法,标记清除算法,标记压缩算法,分代收集思想,stw,CMS回收器,三色标记算法

一、什么是垃圾

当对象没有被任何引用指向的时候,就是垃圾。垃圾对象如果不回收,将会一直占用我们的内存空间,占用内存越来越多,可能会导致内存溢出,而且会产生内存碎片。早期的C++程序需要程序员手动的去清除垃圾,如果程序员忘记清除垃圾就会造成内存泄露

内存溢出:内存被不停的消耗,没有被回收,内存被消耗完,不够使用的情况

内存泄露:该回收的垃圾,但是没有被回收,当内存慢慢被消耗直至消耗完之后就会出现内存溢出

二、Java自动垃圾回收

Java应用的是自动的垃圾回收,程序员不需要再关注内存的清除,可以只需要关注业务代码,降低内存溢出的风险

自动垃圾回收但是也有担忧,如果过度依赖自动垃圾回收,就会降低程序员解决内存溢出或者内存泄露的能力,所以需要程序员对垃圾自动回收有深入了解

垃圾回收主要是针对堆与方法区,在堆里面主要包括新生代与老年代,垃圾回收会频繁回收新生代,较好回收老年代;方法区的回收是在发生Full GC的时候,方法区的回收也较少

三、垃圾回收算法

标记阶段

1.垃圾标记阶段算法

标记阶段的目的是为了判断对象是否存活,也就是判断是否还有引用指向对象

当GC进行垃圾回收的时候的,需要先判断对象是否存活,主要包括引用计数算法(没有使用)与根可达算法

1.1 引用计数算法

当对象被引用的时候,引用指向计数器增加;当一个对象的引用计数器的值是0的时候,就表明没有引用指向该对象,此时该对象就可以被回收;这种算法实现简单;但是这种算法无法解决循环引用的的问题,会导致内存泄露,这也是GC没有使用该算法进行垃圾标记的原因

循环引用:引用P指向对象A,对象A指向对象B,对象B指向对象C,对象C指向对象A,此时对象A的引用数是2,对象B和对象C的引用数是1;但是实际上对象ABC真正的引用都只有引用P,此时引用P指向改变(P指向A对象的引用断开),对象ABC就变成了垃圾(没有实际引用指向),但是他们的引用数不为0,GC就无法回收他们,此时就出现了内存泄露,这也是引用计数法的致命缺陷,参考下图进行理解:

在这里插入图片描述

1.2 根可达算法

这种算法可以避免循环引用,有效的避免内存泄露

GCRoots根集合:一组活跃的引用

根可达算法从GCRoots引用开始从上到下,去寻找对象,如果对象从根对象开始可以直接或者间接的被找到,那么这个对象就不是垃圾,否则这个对象就会被标记为垃圾,可参看下图理解:

在这里插入图片描述

可以被作为GCRoots的元素:

  1. Java虚拟机栈中引用的对象
  2. 本地方法栈引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象
  5. 所有被同步锁synchronized持有的对象
  6. Java虚拟机内部引用的对象

    2.对象的finalization机制

    finalize()方法机制: 对象被销毁前GC会回调一次的方法,在Object中被定义,这个方法可以重写,但是子类不要去主动调用;主动调用的潜在风险较大,比如,让对象死而复生,GC的性能受到影响(方法里面出现死循环)

对象的状态: 当我们的对象没用任何引用指向的时候,此时对象就是垃圾,但是由于finalization机制的存在,此时的对象不会被立即回收,可能会死而复生;因此对象被分为了

三种状态: 可触及的(从GCRoots中可以到达),可复活的(对象已经被标记为垃圾,但是还未执行finalize方法),不可触及的(对象已经执行finalize方法已经执行,并且对象没有被复活);只有在不可触及状态对象才会被回收

对象标记阶段的过程: 当对象在GCRoots引用链中无法到达,则此时对象会被第一次标记,此时是可复活状态;此时这个对象处于待执行finalizae方法的阶段,JVM会判断该对象是否有必要执行finalize方法,如果finalize方法已经执行过,那么此时这个对象状态会转为不可触及状态,也就是可以被GC立即回收;如果JVM判断该对象需要执行finalize方法,则去执行,执行完之后对象死而复生,则对象的状态变成可触及状态,不会被GC回收掉;如果执行之后,该对象没有死而复生,则对象的状态变成不可触及状态,会被GC立即回收掉

3.垃圾回收阶段算法

回收阶段

3.1 标记复制算法

将内存分为大小相等的两块内存(幸存者区域),将正在使用的区域的存活对象复制到未被使用的内存空间,然后将原来的内存空间清空,交换两块空间的角色;实现简单,运行高效,不会出现内存碎片,但是使用的内存空间较大,需要两倍的内存空间;适用于垃圾较多,存活对象少的场景,如新生代可以使用该算法;可参考下图理解:

在这里插入图片描述

3.2 标记清除算法

标记指得是通过可达性算法去标记垃圾对象与存活对象,清除指得是把需要清除的垃圾对象地址保存在空闲的地址列表里面。当下次新对象需要去加载的时候,回去地址列表寻找地址,判断地址的内存空间大小是否够用,如果够用则用新对象替换垃圾对象;容易理解,但是内存空间不连续,会产生内存碎片,效率低;可以参考下图理解:

在这里插入图片描述

3.3 标记压缩算法

复制算法一般在新生代中使用的较多,新生代中存活对象较少,需要移动对象少,效率高,不会产生内存碎片,但是对于老年代对象多不适合复制算法,因为移动的对象较多,不适合;所以在老年代的时候会使用标记清除算法,此时不会进行复制操作,但是会产生内存碎片。针对这个问题使用标记压缩算法,相当于给对象进行了标记清除操作后,又进行了内存碎片整理的操作;可参考下图理解:

在这里插入图片描述

标记压缩算法消除了标记清除算法中内存碎片的问题,但是移动对象的时候会产生新的问题,对象如果多,因为需要移动对象内存开销大,消耗时间

回收阶段三种算法的比较:

在这里插入图片描述

4.分代收集思想

新生代对象存活率低,需要频繁回收,标记复制算法效率高,适合新生代进行垃圾回收

老年代对象的生命周期较长,不需要频繁回收,使用标记清除或者标记压缩算法

5.System.gc()

这个方法是一个显示的触发Full GC方法,但是这个方法又会去调用Runtime.getRuntime().gc()方法,但是调用这个方法后不一定立即去执行。垃圾回收一般情况下是自动的,不需要去手动的回收

6.Stop The Word(stw)

当垃圾回收器回收线程标记的时候,会将所有的用户线程停止下来,保证在判定对象是否是垃圾时的准确性,性能好的垃圾回收器发生STW的次数少

四、垃圾回收器

JVM中有不同类型的垃圾回收器,可以根据使用场景选择对应的垃圾回收器

垃圾回收器根据线程的数量分为:

  1. 单线程垃圾回收器(serial):垃圾回收线程只有一个,适用于小型场景
  2. 多线程垃圾回收器(parallel):垃圾回收线程有多个,适用于处理垃圾较多场景

按照工作模式分为:

  1. 独占式:垃圾回收线程工作时,用户线程会全部暂停
  2. 并发式:垃圾回收线程的时候,用户线程不会全部暂停

按照内存工作区域分为:

  1. 年轻代垃圾回收器
  2. 老年代垃圾回收器

## 1.CMS回收器 并发标记清除回收器;在这个回收器之前垃圾回收器都是独占式的;首创了用户线程与垃圾回收线程并发执行;CMS追求的是低停顿(STW次数减少)

CMS进行了三次标记初始标记,单线程独占标记对象;并发标记,垃圾回收线程和用户线程并发执行(用户线程不再暂停);重新标记:使用多线程独占标记对象; 并发清除:垃圾回收线程与用户线程并发执行

优点:可以做到并发收集

缺点: 用户线程与垃圾收集线程并发执行,导致吞吐量降低;CMS无法清除浮动垃圾(因为用户线程与垃圾收集线程并发执行,第一次标记完垃圾对象,由于用户线程不停止,随时还会产生新的垃圾对象,无法处理,只能等到下次垃圾回收处理);因为使用的是标记清除算法,所以会产生内存碎片

三色标记算法

  1. 黑:GCRoots的对象,表示已经被标记过,且该对象下的属性也被标记过了
  2. 灰:已经被垃圾扫描器扫描过,但是其下属存在没有被标记过的对象
  3. 白:没有被垃圾扫描器扫描过,标记为垃圾

三色标记法的执行过程:GCRoots的对象被标记为黑色;GCRoots直接下属对象被标记为灰色;遍历灰色对象的所有引用,灰色对象本身置为黑色,其直接下属对象置为灰色;重复上一步直至没有灰色对象为止;结束后,黑色对象存活,白色对象回收

这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况

三色标记法会产生漏标错标的现象

漏标: 存在对象ABC,对象A指向B,对象B指向对象C,此时发生第一次三色标记,A对象会被置为黑色,B对象会被置为灰色,C对象会被置为白色;此时A对象指向B对象的引用断开,那么对象B与C是垃圾,需要回收,但是此时B对象已经被标记为了灰色,当进行下一次三色标记的时候,B会变成黑色,C会变成灰色;再进行一次三色标记,C也会变成黑色;因为三色标记结束后黑色留下,所以BC对象会留下;但是在第一次三色标记的时候,BC已经是垃圾了,需要回收,造成漏标;可参考下图理解:

在这里插入图片描述

错标: 存在对象ABC,对象A指向B,对象B指向对象C,此时发生第一次三色标记,A对象会被置为黑色,B对象会被置为灰色,C对象会被置为白色;此时B对象指向C对象的引用断开,A对象指向C对象,此时实际是没有垃圾对象,但是C对象是白色的,并且已经和B对象断开链接;所以下一次三色标记的时候,B对象会变成黑色,但是A已经被扫描过了,不会被再次扫描,C对象之后会一直是白色;直至三色标记结束后,当做垃圾被回收,造成错标;可参考下图理解:

在这里插入图片描述

造成错标的条件有两个:

  1. 灰色直线改白色对象引用断开(原始快照解决)
  2. 黑色指向白色的引用建立(增量更新解决)

原始快照:当灰色对象指向白色对象的引用断开,此时建立关系连接;当三色标记结束后,以灰色为根再次进行三色标记

增量更新:当黑色指向白色的引用建立的时候,此时创建关系连接;当三色标记结束,以黑色为根,再次进行三色标记

原始快照和增量更新是对错标发生的两个条件分别进行了解决,所以判断是否是垃圾的时候,该对象在原始快照和增量更新进行后都必须保证是黑色,否则该对象是垃圾

2.G1回收器

Grade First回收器;它把堆内存空间分割为很多不相关的区域,G1跟踪每个区域中的垃圾,根据优先列表里面的优先级每次优先回收优先级高的区域


 

Content