简述JVM中的垃圾回收机制

简述JVM中的垃圾回收机制

(一)概述

JVM垃圾回收机制是面试的重点也是难点,只要问到JVM,那么垃圾回收是必问的。JVM垃圾回收算法主要的几个核心点包括垃圾识别机制,垃圾回收算法,有时候也需要了解当今商用JVM使用的几种垃圾收集器和他们之间的区别。

我们先来了解一下GC。 java相较于c、c++语言的优势之一是自带垃圾回收器,程序开发人员不用手动管理内存,内存的分配和释放完全由GC(Garbage Collector)来做,极大地提高了软件开发效率及程序健壮性(手动管理内存容易造成内存泄漏)。凡事皆有两面性,java GC在给我们带来内存管理便捷性的同时,也面临STW(Stop The World)影响程序吞吐的缺陷。

由此可见,我们学习GC是非常有必要且非常关键的,下面我们来按顺序看一下GC中的一些细节。

(二)java中的四种引用类型

看到这你可能会很诧异,我们不是在聊GC吗?这四种引用是什么东西?我先不说这些和GC有什么关联,后面自然会明白。四种引用分为强引用、软引用、弱引用、虚引用。我们平时用的最多引用的就是强引用,后面三个完全没听说过?没事,既然他们都是引用,那么功能肯定是差不多的。为了降低难度,我们这里只讲概念,暂时不讲使用场景和案例。下面我们一个个看看:

1. 强引用

我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,就说明他不是垃圾,则不会被垃圾回收。

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

上面的例子非常简单,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收这些对象。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

2. 软引用

软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。

3. 弱引用

弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。换句话说,弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。

4. 虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。在 JDK1.2之后,用PhantomReference类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()方法,而且它的get()方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和ReferenceQueue引用队列一起使用。

这些概念可能有点抽象,不过这四种引用具有不同的垃圾回收时机应该是很清楚的。我们会发现,引用的强度从强、软、弱、虚依次递减,越往后的引用所引用的对象越容易被垃圾回收。

(二)垃圾识别机制

GC的第一步当然是识别垃圾。一个好的垃圾识别机制可以更高效的识别垃圾同时也不会误伤有用的对象,我们来看一下java中的两种垃圾识别机制:

1. 引用计数法

最容易想到的一种方法就是引用计数法。引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是垃圾了。

引用计数器实现简单,效率高,但是不能解决循环引用的问题(A对象引用B对象,B对象又引用A对象,但是A和B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了额外的开销,所以在JDK1.1之后,这个算法已经弃用。

2. 可达性算法

一种听起来比较新颖的方法,也被称为根搜索算法。根搜索算法是通过一些“GCRoots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(ReferenceChain),当一个对象没有被GCRoots的引用链连接的时候,说明这个对象是垃圾,应该被回收。

在这里插入图片描述
那么这些roots应该是哪些对象呢?GCRoots主要是四种:

1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2. 方法区域中的类静态属性引用的对象。
3. 方法区域中常量引用的对象。
4. 本地方法栈中JNI(Native方法)的引用的对象。

(三)垃圾回收算法

确认完垃圾之后肯定要想办法回收垃圾。回收垃圾主要有下面四种方法:标记清除算法、标记整理算法、复制算法、分代收集算法。

1. 标记清除算法

标记清除(Mark-Sweep)算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。标记阶段就是上面讲过的垃圾识别机制,会将有用的对象进行标记。因此,未被标记的对象就是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象。

在这里插入图片描述
我们会发现,标记清除算法使用后,内存中会出现很多的碎片(图中的空白块)。如果这样的碎片很多,则会导致内存的利用率急剧下降。因此我们需要找到能不产生碎片的解决方案。

2. 标记整理算法

标记整理算法类似与标记清除算法,这种算法可以解决碎片问题。原理在于,它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向内存的一端移动覆盖,然后直接清理掉边界以外的内存。

在这里插入图片描述
虽然这个方法可以完美的解决碎片问题,但是移动对象必然导致效率的降低,尤其是对象存活率较高的时候这个效率问题会更加明显。

3. 复制算法

复制算法可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收。

在这里插入图片描述
虽然部分解决了效率问题,但是这种方法会浪费一半的内存空间。

4. 分代回收算法

当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。而老年代因为对象存活率高,没有额外空间对它进行分配担保,就使用“标记清理”或者“标记整理”算法来进行回收。

在这里插入图片描述
把空间划分成年轻代和老年代两块。创建新的对象总是先在年轻代里创建,当年轻代空间不足了,就会触发年轻代的GC,我们称之为YGC(也被称为minor GC),经过一次YGC,对象的年龄就加1,当对象的年龄达到一个阈值(可以自己设定,默认为15)时,这个对象就可以进入老年代了。

年轻代分为伊甸园区和幸存区,幸存区分为from和to区。一般情况下,年轻代占1/3,老年代占2/3。然后伊甸园区占8/10,幸存区占2/10,from和to区各占其中1/2。每次执行YGC,会将伊甸园区和from区存活的对象放到to区(如果to区不够,则直接进入老年代),然后将from区和to区的职责交换(from区变为to区,to区变为from区)。

放入对象时,对象先会来到伊甸园区,如果伊甸园的剩余内存大小可以放下,就直接放到伊甸园区,如果伊甸园内存不够,就会进行YGC并将存活对象的将分代年龄进行加1。YGC回收时,会将伊甸园区的垃圾和幸存区的垃圾都会进行回收。如果对象来到伊甸园区,对象放不下,又到from区也放不下,就会放入老年代,直到老年代放不下的时候,先会进行一次YGC ,如果进行完之后,还是内存不够,就会进行full GC,full GC会将新生代和老年代都会进行回收,并且会使其他的进程全部停止,称为stop the word(STW),如果进行full GC之后,内存还是不够的时候,就会抛出异常OOM。

年轻代的空间总是会比老年代先满,所以年轻代的回收也更频繁。如果能撑过多次YGC的,这种对象的生命周期往往会非常长,那么可以把它们放到老年代,避免在YGC时被拷来拷去,能够有效地提升GC效率。

在程序中可能存在年老代对象引用新生代对象的情况,如果需要执行YGC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是非常低效的。解决的方法是,年老代中维护一个512 byte的块——“card table”,所有老年代对象引用新生代对象的记录都记录在这里。YGC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

(四)常见的垃圾回收器简述

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、版本的虚拟机所提供的垃圾收集器都可能会有很大差别。

在这里插入图片描述
这里是一些常见的垃圾收集器,上半部分是YGC的垃圾收集器,下面是full GC的垃圾收集器,两个垃圾收集器之间有线连接则代表可以配合使用。

我们先来了解一下可能会见到的一些概念:

  1. 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  2. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
  3. 新生代GC(YGC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以YGC非常频繁,一般回收速度也比较快。
  4. 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了full GC,经常会伴随至少一次的YGC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行full GC的策略选择过程)。full GC的速度一般会比YGC慢10倍以上。
  5. 吞吐量:吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

如果当垃圾回收器进行垃圾清理时,必须暂停其他所有的工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为Stop-the-World。以上回收器中,Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old均采用的是Stop-the-World的策略。

这里只是大概介绍了一下一些垃圾收集器,具体的我会在另一篇文章中讲,我会聚焦于每个垃圾收集器的特性和优势。

2020年8月10日

猜你喜欢

转载自blog.csdn.net/weixin_43907422/article/details/107774213