关于后端:垃圾收集策略与算法

47次阅读

共计 3449 个字符,预计需要花费 9 分钟才能阅读完成。

垃圾收集策略与算法

程序计数器、虚拟机栈、本地办法栈随线程而生,也随线程而灭;栈帧随着办法的开始而入栈,随着办法的完结而出栈。这几个区域的内存调配和回收都具备确定性,在这几个区域内不须要过多思考回收的问题,因为办法完结或者线程完结时,内存天然就跟随着回收了。

而对于 Java 堆和办法区,咱们只有在程序运行期间能力晓得会创立哪些对象,这部分内存的调配和回收都是动静的,垃圾收集器所关注的正是这部分内存。

断定对象是否存活

若一个对象不被任何对象或变量援用,那么它就是有效对象,须要被回收。

援用计数法

在对象头保护着一个 counter 计数器,对象被援用一次则计数器 +1;若援用生效则计数器 -1。当计数器为 0 时,就认为该对象有效了。

援用计数算法的实现简略,断定效率也很高,在大部分状况下它都是一个不错的算法。然而支流的 Java 虚拟机里没有选用援用计数算法来治理内存,次要是因为它很难解决对象之间循环援用的问题。(尽管循环援用的问题可通过 Recycler 算法解决,然而在多线程环境下,援用计数变更也要进行低廉的同步操作,性能较低,晚期的编程语言会采纳此算法。)

举个栗子 👉 对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,因为它们相互援用着对方,导致它们的援用计数都不为 0,于是援用计数算法无奈告诉 GC 收集器回收它们。

可达性分析法

所有和 GC Roots 间接或间接关联的对象都是无效对象,和 GC Roots 没有关联的对象就是有效对象。

GC Roots 是指:

  • Java 虚拟机栈(栈帧中的本地变量表)中援用的对象
  • 本地办法栈中援用的对象
  • 办法区中常量援用的对象
  • 办法区中类动态属性援用的对象

GC Roots 并不包含堆中对象所援用的对象,这样就不会有循环援用的问题。

援用的品种

断定对象是否存活与“援用”无关。在 JDK 1.2 以前,Java 中的援用定义很传统,一个对象只有被援用或者没有被援用两种状态,咱们心愿能形容这一类对象:当内存空间还足够时,则保留在内存中;如果内存空间在进行垃圾收集后还是十分缓和,则能够摈弃这些对象。很多零碎的缓存性能都合乎这样的利用场景。

在 JDK 1.2 之后,Java 对援用的概念进行了裁减,将援用分为了以下四种。不同的援用类型,次要体现的是对象不同的可达性状态 reachable 和垃圾收集的影响。

强援用(Strong Reference)

相似 “Object obj = new Object()” 这类的援用,就是强援用,只有强援用存在,垃圾收集器永远不会回收被援用的对象。然而,如果咱们 谬误地放弃了强援用,比方:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存透露。

软援用(Soft Reference)

软援用是一种绝对强援用弱化一些的援用,能够让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软援用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软援用指向的对象。软援用通常用来 实现内存敏感的缓存,如果还有闲暇内存,就能够临时保留缓存,当内存不足时清理掉,这样就保障了应用缓存的同时,不会耗尽内存。

弱援用(Weak Reference)

弱援用的 强度比软援用更弱 一些。当 JVM 进行垃圾回收时,无论内存是否短缺,都会回收 只被弱援用关联的对象。

虚援用(Phantom Reference)

虚援用也称幽灵援用或者幻影援用,它是 最弱 的一种援用关系。一个对象是否有虚援用的存在,齐全不会对其生存工夫形成影响。它仅仅是提供了一种确保对象被 finalize 当前,做某些事件的机制,比方,通常用来做所谓的 Post-Mortem 清理机制。

回收堆中有效对象

对于可达性剖析中不可达的对象,也并不是没有存活的可能。

断定 finalize() 是否有必要执行

JVM 会判断此对象是否有必要执行 finalize() 办法,如果对象没有笼罩 finalize() 办法,或者 finalize() 办法曾经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。

如果对象被断定为有必要执行 finalize() 办法,那么对象会被放入一个 F-Queue 队列中,虚构机会以较低的优先级执行这些 finalize()办法,但不会确保所有的 finalize() 办法都会执行完结。如果 finalize() 办法呈现耗时操作,虚拟机就间接进行指向该办法,将对象革除。

对象新生或死亡

如果在执行 finalize() 办法时,将 this 赋给了某一个援用,那么该对象就新生了。如果没有,那么就会被垃圾收集器革除。

任何一个对象的 finalize() 办法只会被零碎主动调用一次,如果对象面临下一次回收,它的 finalize() 办法不会被再次执行,想持续在 finalize() 中自救就生效了。

回收办法区内存

办法区中寄存生命周期较长的类信息、常量、动态变量,每次垃圾收集只有大量的垃圾被革除。办法区中次要革除两种垃圾:

  • 废除常量
  • 无用的类

断定废除常量

只有常量池中的常量不被任何变量或对象援用,那么这些常量就会被革除掉。比方,一个字符串 “bingo” 进入了常量池,然而以后零碎没有任何一个 String 对象援用常量池中的 “bingo” 常量,也没有其它中央援用这个字面量,必要的话,”bingo” 常量会被清理出常量池。

断定无用的类

断定一个类是否是“无用的类”,条件较为刻薄。

  • 该类的所有对象都曾经被革除
  • 加载该类的 ClassLoader 曾经被回收
  • 该类的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

一个类被虚拟机加载进办法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进办法区时创立,在办法区该类被删除时革除。

垃圾收集算法

学会了如何断定有效对象、无用类、废除常量之后,残余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个:

标记 - 革除算法

标记 的过程是:遍历所有的 GC Roots,而后将所有 GC Roots 可达的对象 标记为存活的对象

革除 的过程将遍历堆中所有的对象,将没有标记的对象全副革除掉。与此同时,革除那些被标记过的对象的标记,以便下次的垃圾回收。

这种办法有两个 有余

  • 效率问题:标记和革除两个过程的效率都不高。
  • 空间问题:标记革除之后会产生大量不间断的内存碎片,碎片太多可能导致当前须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

复制算法(新生代)

为了解决效率问题,“复制”收集算法呈现了。它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块内存用完,须要进行垃圾收集时,就将存活者的对象复制到另一块下面,而后将第一块内存全副革除。这种算法有优有劣:

  • 长处:不会有内存碎片的问题。
  • 毛病:内存放大为原来的一半,节约空间。

为了解决空间利用率问题,能够将内存分为三块:Eden、From Survivor、To Survivor,比例是 8:1:1,每次应用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才应用的 Survivor 空间。这样只有 10% 的内存被节约。

然而咱们无奈保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,须要依赖其余内存(指老年代)进行调配担保。

调配担保

为对象分配内存空间时,如果 Eden+Survivor 中闲暇区域无奈装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后仍然有超过 10% 的对象存活,这样存活的对象间接通过调配担保机制进入老年代,而后再将新对象存入 Eden 区。

标记 - 整顿算法(老年代)

标记 :它的第一个阶段与 标记 - 革除算法 是截然不同的,均是遍历 GC Roots,而后将存活的对象标记。

整顿 :挪动所有 存活的对象,且依照内存地址秩序顺次排列,而后将末端内存地址当前的内存全副回收。因而,第二阶段才称为整顿阶段。

这是一种老年代的垃圾收集算法。老年代的对象个别寿命比拟长,因而每次垃圾回收会有大量对象存活,如果采纳复制算法,每次须要复制大量存活的对象,效率很低。

分代收集算法

依据对象存活周期的不同,将内存划分为几块。个别是把 Java 堆分为新生代和老年代,针对各个年代的特点采纳最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记 - 革除算法、标记 - 整顿算法

本文由 mdnice 多平台公布

正文完
 0