一文了解JVM垃圾回收机制和常用算法

1,129 阅读17分钟

垃圾收集 (Garbage Collection,GC)

垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。

判断一个对象是否可被回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

image.png

可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收。

GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收

image.png

Java 虚拟机使用可达性分析算法来判断对象是否可被回收,GC Roots 一般包含以下几种:

  • 虚拟机栈中局部变量表中引用的对象(栈帧中的本地方法变量表)
  • 本地方法栈中 JNI(Native方法) 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 活跃线程的引用对象

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

finalize()

类似 C++ 的析构函数(注意:只是类似,C++ 析构函数调用确定,而 finalize() 方法是不确定的),用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当垃圾回收器要宣告一个对象死亡时,至少要经历两次标记过程。

如果对象在进行可达性分析以后,没有与GC Root 直接相连接的引用量,就会被第一次标记,并且判断是否执行 finalize() 方法;

如果这个对象覆盖了 finalize() 方法,并且未被引用,就会被放置于 F-Queue 队列,稍后由虚拟机创建的一个低优先级的 finalize() 线程去执行触发 finalize() 方法,在该方法中让对象重新被引用,从而实现自救。但是该线程的优先级比较低,执行过程随时可能会被终止。此外,自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

引用类型

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

JDK1.2 之前,Java 中引用定义:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。

强引用 (Strong Reference)

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();

当内存空间不足,JVM 抛出 OOM Error 终止程序也不会回收具有强引用的对象,只有通过将对象设置为 null 来弱化引用,才能使其被回收。

软引用 (Soft Reference)

表示对象处在有用但非必须的状态。

被软引用关联的对象只有在内存不够的情况下才会被回收。可以用来实现内存敏感的高速缓存。

软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。

使用 SoftReference 类来创建软引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

弱引用 (Weak Reference)

表示非必须的对象,比软引用更弱一些。适用于偶尔被使用且不影响垃圾收集的对象。

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,JVM 就会把这个弱引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。

使用 WeakReference 类来创建弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

虚引用 (Phantom Reference)

又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

不会决定对象的生命周期,任何时候都可能被垃圾回收器回收。必须和引用队列 ReferenceQueue 联合使用

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知,起哨兵作用。具体来说,就是通过判断引用队列 ReferenceQueue 是否加入虚引用来判断被引用对象是否被 GC(垃圾回收线程) 回收:当 GC 准备回收一个对象时,如果发现它还仅有虚引用指向它,就会在回收该对象之前,把这个虚引用加入到与之关联的引用队列 ReferenceQueue 中。如果一个虚引用对象本身就在引用队列中,就说明该引用对象所指向的对象被回收了

使用 PhantomReference 来创建虚引用。

Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue()
// 虚引用必须和引用队列 ReferenceQueue 联合使用
PhantomReference<Object> pf = new PhantomReference<Object>(obj, queue);
obj = null;

总结:

引用类型被垃圾回收的时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足的时候对象缓存内存不足时终止
弱引用在垃圾回收的时候对象缓存GC运行后终止
虚引用Unknown标记、哨兵Unknown

垃圾回收算法

“标记-清除” 算法

image.png

标记清除算法,是将垃圾回收分为2个阶段,分别是标记清除

在标记阶段,从根集合进行扫描,会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记(可达性算法进行标记)。

在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。

在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。

  • 优点:标记和清除速度较快
  • 缺点:碎片化较为严重,内存不连贯的

”标记-整理“ 算法

image.png

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • 优点:不会产生内存碎片
  • 不足:需要移动大量对象,处理效率比较低。

”复制“ 算法

image.png

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

主要不足:只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

分代回收算法

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

在java8时,堆被分为了两份:新生代和老年代【1:2】

image.png

对于新生代,内部又被分为了三个区域。

  • 伊甸园区Eden,新生的对象都分配到这里
  • 幸存者区survivor(分成from和to)
  • Eden区,from区,to区【8:1:1】

分代收集算法-工作机制

  1. 新创建的对象,都会先分配到eden区

image.png

  1. 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象。 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放。

image.png

  1. 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区

image.png

  1. 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区。
  2. 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

image.png

MinorGC、 Mixed GC 、 FullGC的区别是什么

  1. Minor GC(新生代垃圾回收):

    • 发生在新生代内存区域(通常是Eden区和Survivor区)。
    • 主要回收生命周期短的对象,即存活时间较短的对象。
    • 通常使用复制算法来进行垃圾回收,将存活的对象复制到Survivor区域,并清理掉不再使用的对象。
    • Minor GC的目标是保持新生代的空间可用,以便存放新对象。
  2. Mixed GC(混合垃圾回收):

    • 发生在新生代和老年代之间。
    • 将新生代中存活的对象移动到老年代,以减少新生代的内存压力。
    • 通常在新生代空间不足时,会触发Mixed GC,它的目标是在新生代和老年代之间平衡内存使用。
  3. Full GC(老年代垃圾回收):

    • 主要回收生命周期较长的对象,即存活时间较长的对象。
    • 通常会涉及到对整个堆内存的垃圾回收,包括新生代和老年代。
    • Full GC的目标是回收整个堆内存的空间,以便释放不再使用的对象,避免内存泄漏。

Stop-the-World

所谓 Stop-the-World(简称 STW),指的是 JVM 由于要执行 GC 而停止了应用程序的执行 

可达性分析算法中 GC Roots 会导致所有 Java 执行线程停顿,原因如下:

  • 分析工作必须在一一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 影响用户体验,所以需要减少 STW 的发生。

STW 事件和采用哪款垃圾收集器无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 STW 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用 System.gc()  这样会导致 STW 的发生。

目前,降低系统的停顿时间两种算法:增量收集算法和分区算法。

增量收集算法

基本思想:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。垃圾收集线程一次只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

不足:由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

基本思想:一般来说,在相同条件下,堆空间越大,一次 GC 时所需要的时间就长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。

注意分区算法与分代收集算法是不同的:分代收集算法将按照对象的生命周期长短划分成两个部分,而分区算法将整个堆空间划分成连续的不同小区间,其中每个小区间都独立使用,独立回收,这样可以控制一次回收多少个小区间。

总结:

  • 增量收集算法是将总的收集量一部分一部分的去执行
  • 分区算法是将总的内存空间分为小分区,一次可控的去收集多少个小区间。

垃圾回收器

串行垃圾收集器

SerialSerial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑。

  • Serial 作用于新生代,采用复制算法
  • Serial Old 作用于老年代,采用标记-整理算法

垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

image.png

并行垃圾收集器

Parallel NewParallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

  • Parallel New作用于新生代,采用复制算法
  • Parallel Old作用于老年代,采用标记-整理算法

垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

image.png

CMS垃圾收集器

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。

image.png

G1垃圾回收器

应用于新生代和老年代,在JDK9之后默认使用G1

  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 采用复制算法
  • 响应时间与吞吐量兼顾

分成三个阶段:新生代回收、并发标记、混合收集 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

image.png

Young Collection(年轻代垃圾回收)

  1. 初始时,所有区域都处于空闲状态
  2. 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
  3. 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程

image.png

  1. 随着时间流逝,伊甸园的内存又有不足
  2. 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

image.png

Young Collection + Concurrent Mark (年轻代垃圾回收+并发标记)

  1. 当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程

image.png

  1. 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。
  2. 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。

image.png

Mixed Collection (混合垃圾回收)

  1. 混合收集阶段中,参与复制的有 eden、survivor、old

image.png

  1. 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

image.png

总结(简介版)

image.png