共计 3496 个字符,预计需要花费 9 分钟才能阅读完成。
原创:码农参上(微信公众号 ID:CODER_SANJYOU),欢送分享,转载请保留出处。
提到 Java 中的垃圾回收,我置信很多小伙伴和我一样,第一反馈就是面试必问了,你要是没背过点 GC 算法、收集器什么的常识,出门都不敢说本人背过八股文。说起来还真是有点难堪,工作中理论用到这方面常识的场景真是不多,并且这货色学起来也很干燥,然而奈何面试官就是爱问,咱们能有什么方法呢?
既然曾经卷成了这样,不学也没有方法,Hydra 就义了周末工夫,给大家画了几张动图,心愿通过这几张图,可能帮忙大家对垃圾收集算法有个更好的了解。废话不多说,首先还是从根底问题开始,看看怎么判断一个对象是否应该被回收。
判断对象存活
垃圾回收的基本目标是利用一些算法进行内存的治理,从而无效的利用内存空间,在进行垃圾回收前,须要判断对象的存活状况,在 jvm 中有两种判断对象的存活算法,上面别离进行介绍。
1、援用计数算法
在对象中增加一个援用计数器,每当有一个中央援用它时计数器就加 1,当援用生效时计数器减 1。当计数器为 0 的时候,示意以后对象能够被回收。
这种办法的原理很简略,判断起来也很高效,然而存在两个问题:
- 堆中对象每一次被援用和援用革除时,都须要进行计数器的加减法操作,会带来性能损耗
- 当两个对象互相援用时,计数器永远不会 0。也就是说,即便这两个对象不再被程序应用,依然没有方法被回收,通过上面的例子看一下循环援用时的计数问题:
public void reference(){A a = new A();
B b = new B();
a.instance = b;
b.instance = a;
}
援用计数的变动过程如下图所示:
能够看到,在办法执行实现后,栈中的援用被开释,然而留下了两个对象在堆内存中循环援用,导致了两个实例最初的援用计数都不为 0,最终这两个对象的内存将始终得不到开释,也正是因为这一缺点,使援用计数算法并没有被理论利用在 gc 过程中。
2、可达性剖析算法
可达性剖析算法是 jvm 默认应用的寻找垃圾的算法,须要留神的是,尽管说的是 寻找垃圾,但实际上可达性剖析算法寻找的是依然存活的对象。至于这样设计的理由,是因为如果间接寻找没有被援用的垃圾对象,实现起来绝对简单、耗时也会比拟长,反过来标记存活的对象会更加省时。
可达性剖析算法的基本思路就是,以一系列被称为 GC Roots 的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为 援用链,当一个对象到 GC Roots 没有任何援用链相连时,证实该对象不再存活,能够作为垃圾被回收。
在 java 中,可作为 GC Roots 的对象有以下几种:
- 在虚拟机栈(栈帧的本地变量表)中援用的对象
- 在办法区中动态属性援用的对象
- 在办法区中常量援用的对象
- 在本地办法栈中 JNI(
native
办法)援用的对象 - jvm 外部的援用,如根本数据类型对应的 Class 对象、一些常驻异样对象等,及零碎类加载器
- 被同步锁
synchronized
持有的对象援用 - 反映 jvm 外部状况的
JMXBean
、JVMTI
中注册的回调本地代码缓存等 - 此外还有一些 临时性 的 GC Roots,这是因为垃圾收集大多采纳 分代收集 和部分回收,思考到跨代或跨区域援用的对象时,就须要将这部分关联的对象也增加到 GC Roots 中以确保准确性
其中比拟重要、同时提到的比拟多的还是后面 4 种,其余的简略理解一下即可。在理解了 jvm 是如何寻找垃圾对象之后,咱们来看一看不同的垃圾收集算法的执行过程是怎么的。
垃圾收集算法
1、标记 - 革除算法
标记革除算法是一种十分根底的垃圾收集算法,当堆中的无效内存空间耗尽时,会触发 STW(stop the world
),而后分 标记 和革除 两阶段来进行垃圾收集工作:
- 标记:从 GC Roots 的节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象
- 革除:对整个堆内存空间进行扫描,如果发现某个对象未被标记为可达对象,那么将其回收
通过上面的图,简略的看一下两阶段的执行过程:
然而这种算法会带来几个问题:
- 在进行 GC 时会产生 STW,进行整个应用程序,造成用户体验较差
- 标记和革除两个阶段的效率都比拟低,标记阶段须要从根汇合进行扫描,革除阶段须要对堆内所有的对象进行遍历
- 仅对非存活的对象进行解决,革除之后会产生大量不间断的内存碎片。导致之后程序在运行时须要调配较大的对象时,无奈找到足够的间断内存,会再触发一次新的垃圾收集动作
此外,jvm 并不是真正的把垃圾对象进行了遍历,把外部的数据都删除了,而是把垃圾对象的首地址和尾地址进行了保留,等到再次分配内存时,间接去地址列表中调配,通过这一措施进步了一些标记革除算法的效率。
2、复制算法
复制算法次要被利用于新生代,它将内存分为大小雷同的两块,每次只应用其中的一块。在任意工夫点,所有动态分配的对象都只能调配在其中一个内存空间,而另外一个内存空间则是闲暇的。复制算法能够分为两步:
- 当其中一块内存的无效内存空间耗尽后,jvm 会进行利用程序运行,开启复制算法的 gc 线程,将还存活的对象复制到另一块闲暇的内存空间。复制后的对象会严格依照内存地址顺次排列,同时 gc 线程会更新存活对象的内存援用地址,指向新的内存地址
- 在复制实现后,再把应用过的空间一次性清理掉,这样就实现了应用的内存空间和闲暇内存空间的对调,使每次的内存回收都是对内存空间的一半进行回收
通过上面的图来看一下复制算法的执行过程:
复制算法的的长处是补救了标记革除算法中,会呈现内存碎片的毛病,然而它也同样存在一些问题:
- 只应用了一半的内存,所以内存的利用率较低,造成了节约
- 如果对象的存活率很高,那么须要将很多对象复制一遍,并且更新它们的利用地址,这一过程破费的工夫会十分的长
从下面的毛病能够看出,如果须要应用复制算法,那么有一个前提就是要求对象的存活率要比拟低才能够,因而,复制算法更多的被用于对象“朝生暮死”产生更多的新生代中。
3、标记 - 整顿算法
标记整顿算法和标记革除算法十分的相似,次要被利用于老年代中。可分为以下两步:
- 标记:和标记革除算法一样,先进行对象的标记,通过 GC Roots 节点扫描存活对象进行标记
- 整顿:将所有存活对象往一端闲暇空间挪动,依照内存地址顺次排序,并更新对应援用的指针,而后清理末端内存地址以外的全副内存空间
标记整顿算法的执行过程如下图所示:
能够看到,标记整顿算法对后面的两种算法进行了改良,肯定水平上补救了它们的毛病:
- 绝对于标记革除算法,补救了呈现内存空间碎片的毛病
- 绝对于复制算法,补救了节约一半内存空间的毛病
然而同样,标记整顿算法也有它的毛病,一方面它要标记所有存活对象,另一方面还增加了对象的挪动操作以及更新援用地址的操作,因而标记整顿算法具备更高的应用老本。
4、分代收集算法
实际上,java 中的垃圾回收器并不是只应用的一种垃圾收集算法,以后大多采纳的都是分代收集算法。jvm 个别依据对象存活周期的不同,将内存分为几块,个别是把堆内存分为新生代和老年代,再依据各个年代的特点抉择最佳的垃圾收集算法。次要思维如下:
- 新生代中,每次收集都会有大量对象死去,所以能够抉择 复制 算法,只须要复制大量对象以及更改援用,就能够实现垃圾收集
- 老年代中,对象存活率比拟高,应用复制算法不能很好的进步性能和效率。另外,没有额定的空间对它进行调配担保,因而抉择 标记革除 或标记整顿 算法进行垃圾收集
通过图来简略看一下各种算法的次要利用区域:
至于为什么在某一区域抉择某种算法,还是和三种算法的特点非亲非故的,再从 3 个维度进行一下比照:
- 执行效率:从算法的工夫复杂度来看,复制算法最优,标记革除次之,标记整顿最低
- 内存利用率:标记整顿算法和标记革除算法较高,复制算法最差
- 内存参差水平:复制算法和标记整顿算法较参差,标记革除算法最差
只管具备很多差别,然而除了都须要进行标记外,还有一个相同点,就是在 gc 线程开始工作时,都须要 STW
暂停所有工作线程。
总结
本文中,咱们先介绍了垃圾收集的根本问题,什么样的对象能够作为垃圾被回收?jvm 中通过可达性剖析算法解决了这一关键问题,并在它的根底上衍生出了多种罕用的垃圾收集算法,不同算法具备各自的优缺点,依据其特点被利用于各个年代。
尽管这篇文章唠唠叨叨了这么多,不过这些都还是根底的常识,如果想要彻底的把握 jvm 中的垃圾收集,后续还有垃圾收集器、内存调配等很多的常识须要了解,不过咱们明天就介绍到这里啦,心愿通过这一篇图解,可能帮忙大家更好的了解垃圾收集算法。
最初,提前祝大家国庆小长假欢快,咱们下篇见~
作者简介,码农参上(CODER_SANJYOU),一个酷爱分享的公众号,乏味、深刻、间接,与你聊聊技术。集体微信 DrHydra9,欢送增加好友,进一步交换。