乐趣区

关于java:JVM知识总结3垃圾收集策略与算法

【JVM 常识总结 -1】JVM 内存模型
【JVM 常识总结 -2】HotSpot 虚拟机对象
【JVM 常识总结 -3】垃圾收集策略与算法

程序计数器、虚拟机栈、本地办法栈随线程而生,也随线程而灭;栈帧随着办法的开始而入栈,随着办法的完结而出栈。这几个区域的内存调配和回收都确定性,在这几个区域不须要过多的思考回收的问题,因为办法完结或者线程完结,内存天然就跟随着回收了。
而对于 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 并不包含堆中对象所援用的对象,这样就不会有循环援用的问题。

援用的品种

断定对象是否存在活与“援用”无关。在 JDK1.2 以前,Java 中的援用定义很传统,一个对象只有被援用或者没有被援用两种状态,咱们心愿能形容这一景象:当内存空间还足够时,则多保留在内存中;如果内存空间在进行垃圾收集后还十分缓和,则能够摈弃这些对象。很多零碎的缓存性能复合这样的应用场景。
在 JDK1.2 之后,Java 对援用的概念进行了裁减,将援用分为了一下四种。不同的援用类型,次要体现的是对象不同的可达性状态 reachable 和垃圾收集的影响。

强援用(Strong Reference)

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

软援用(Soft Reference)

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

弱援用(Weak Reference)

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

虚援用(Phantom Reference)

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

回收堆中有效对象

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

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

JVM 会判断此对象是否有必要执行 finalized()办法,如果对象没有笼罩 finalized()办法,或者 finalize()办法曾经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。
如果对象被断定为有必要执行 finalize()办法,那么对象会被放入一个 F -Queue 队列中,虚构机会以比拟低的优先级执行这些 finalize()办法,但不会确保所有的 finalize()办法都会执行完结。如果 finalize()办法呈现耗时操作,虚拟机就间接进行指向该办法。将对象革除。

对象新生或死亡

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

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

回收办法区内存

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

  • 废除常量
  • 无用的类

断定废除常量

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

断定无用的类

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

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

一个类被虚拟机加载进办法区,那么在堆中就会有一个代表该类的对象: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 堆内分为新生代和老年代,针对各个年代的特点采纳最合适的收集算法。

  • 新生代:复制算法
  • 老年代:标记 - 革除算法、标记 - 整顿算法
退出移动版