共计 4047 个字符,预计需要花费 11 分钟才能阅读完成。
对象已死?
在堆外面寄存着 Java 中简直所有的对象实例,垃圾收集器在对堆进行回收前,第一件工夫就是要确定哪些对象还 “ 存活 ” 着,哪些曾经 “ 死去 ”(代表即不可能再被任何路径应用的对象)了。
援用计数算法
原理
在对象中增加一个援用计数器,每当有一个中央援用它时,计数器值就加 1;当援用生效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被应用的。
长处
- 简略间接
-
断定效率高
毛病
占用了额定的内存空间进行计数
- 须要额定的内存空间来进行计数
- 无奈解决对象之间的互相循环依赖问题
对于相互循环援用,请看以下代码,testGC() 办法中有对象 objA 和 objB,对象属性中有 instance,赋值 objA.instance = objB 及 objB.instance = objA, 除此之外,这两个对象再无任何援用,实际上两个对象曾经不可能再被拜访,然而它们因为互相援用着对方,导致它们的援用计数都不为 0,援用计数器算法也就无奈回收它们。
执行后果
从运行后果中能够看清楚内存回收日之中蕴含 “6717K->608K”,意味着虚拟机并没有因为这两个对象互相援用就放弃回收它们,这也从侧面阐明了 Java 虚拟机并不是通过援用计数算法来判断对象是否存活的。
可达性剖析算法
原理
可达性剖析算法的基本思路就是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,依据援用关系向下搜寻,搜寻过程所走过的门路称为 “ 援用链 ”(Reference Chain),如果某个对象到 GC Roots 间没有任何援用链相连,或者用图论到话来说就是从 GC Roots 到这个对象不可达时,则证实此对象时不可能再被应用的。
如下图所示,对象 obj5、obj6、obj7 尽管有关联,然而它们到 GC Roots 是不可达到,因而他们将会断定为可回收的对象。
在 Java 中,固定可作为 GC Roots 的对象包含以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中援用的对象,譬如以后正在运行的办法所用到的参数、局部变量、长期变量等。
- 在办法区中类动态属性援用的对象,譬如 Java 类的援用类型动态变量。
- 在办法区中常量援用的对象,譬如字符串常量池(String Table)里的援用。
- 在本地办法栈中 JNI(通常所说的 Native 办法)援用的对象。
- Java 虚拟机外部的援用,如根本类型对应的 Class 对象,一些常驻的异样对象(比方 NullPointException、OutOfMemoryError)等,还有零碎类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机外部状况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
长处
可达性剖析能够解决援用计数器所不能解决的循环援用问题
目前最新的几款垃圾回收器无一例外都具备了部分回收的特色,为了防止 GC Roots 蕴含过多对象而适度收缩,它们在实现上也做出了各种优化解决。
再谈援用
无论是通过援用计数算法判断对象的援用数量,还是通过可达性剖析算法判断对象是否援用链可达,断定对象是否存活和 “ 援用 ” 离不开关系。在 JDK1.2 版之前,Java 外面的援用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另一块内存的起始地址,就称该 reference 数据时代表某块内存、某个对象的援用。这种定义并没有什么不对,只是当初看来有些过于狭窄了,一个对象在这种定义下只有 “ 被援用 ” 或者 “ 未被援用 ” 两种状态,对于形容一些 “ 食之无味,弃之可惜 ” 的对象就显得无能为力。譬如咱们心愿能形容一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后依然十分缓和,那就能够摈弃这些对象 – 很多零碎的缓存性能都合乎这样的利用场景。
在 JDK1.2 版之后,Java 对援用的对象进行了裁减,将援用分为强援用(Strongly Reference)、软援用(Soft Reference)、弱援用(Weak Reference)和虚援用(Phantom Reference)4 种,这 4 种援用强度顺次削弱。
- 强援用是最传统的 “ 援用 ” 的定义,是指在程序代码之中普遍存在的援用赋值,即相似 “Object obj = new Object()” 这种援用关系。无论任何状况下,只有强援用关系还存在,还可达,垃圾收集器就永远不会回收掉被援用的对象。
- 软援用是用来形容一些还有用,但非必须的对象。只被软援用关联着的对象,在零碎将要产生内存溢出异样前,会把这些对象列进回收范畴之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异样。在 JDK1.2 之后提供了 SoftReference 类来实现软援用。
- 弱援用也是用来形容那些非必须对象,然而它的强度比软援用更弱一些,被弱援用关联的对象只能生存到下一次垃圾收集产生为止。当垃圾收集器开始工作,无论以后内存是否足够,都会回收掉只被弱援用关联的对象。在 JDK1.2 版本之后提供了 WeakReference 类来实现若援用。
- 虚援用也称为 “ 幽灵援用 ” 或者 “ 幻影援用 ”,它是最弱的一种援用关系。一个对象是否有虚援用的存在,齐全不会对其生存工夫形成影响,也无奈通过虚援用来去的一个对象实例。为一个对象设置虚援用关联的惟一目标只是为了能在这个对象被收集器回收时收到一个零碎告诉。在 JDK1.2 版之后提供了 PhantomReference 类来实现虚援用。
生存 or 死亡?
即便在可达性剖析算法中被断定为不可达到对象,也不是 “ 非死不可 ” 的,这时候它们还谢世处于 “ 缓刑 ” 阶段,要真正宣告一个对象死亡,最多会经验两次标记过程:如果对象在进行可达剖析之后发现没有与 GC Toots 相连接的援用链,那他将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 办法。如果对象没有笼罩 finalize() 办法,或者 finalize() 办法曾经被虚拟机调用过,那么虚拟机将这两种状况都视为 “ 没有必要执行 ”。
如果这个对象被断定为确有必要执行 finalize() 办法,那么该对象将会被搁置在一个 F -Queue 的队列之中,并在稍后由一条由虚拟机主动建设的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 办法。这里所说的 “ 执行 ” 是指虚构机会触发这个办法开始运行,但并不承诺肯定会期待它运行完结。这样做的起因是,如果某个对象的 finalize() 办法执行迟缓,或者更极其地产生了死循环,将很可能导致 F -Queue 队列中的其余对象永恒处于期待,甚至导致整个内存回收子系统的解体。finalize() 办法是对象逃脱死亡的最初一次机会,稍后收集器将堆 F -Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中胜利援救本人————只有从新与援用链上的任何一个对象建设关联即可,譬如把本人(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移出 “ 行将回收 ” 的汇合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。从以下代码中咱们能够看到一个对象的 finalize() 被执行,然而它仍然能够存活。
/**
* 代码演示了两点:* 1. 对象能够在被 GC 时自我援救。* 2. 这种自救的机会只有一次,因为一个对象的 finalize() 办法最左只会被零碎主动调用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {System.out.println("是的,我还活着!");
}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("finalize 办法被执行!");
// 被动态属性援用,可实现自救,在办法区中类动态属性援用的对象,譬如 Java 类的援用类型动态变量可作为 GC Roots
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次胜利援救本人
SAVE_HOOK = null;
System.gc();
// 因为 Finalizer 办法优先级很低,暂停 0.5 秒,期待它
TimeUnit.MILLISECONDS.sleep(500);
if (SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("我曾经死了");
// 上面的代码与下面完全相同,然而这次却自救失败了(一个对象自救的机会只有一次)SAVE_HOOK = null;
System.gc();
// 因为 Finalizer 办法优先级很低,暂停 0.5 秒,期待它
TimeUnit.MILLISECONDS.sleep(500);
if (SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("我曾经死了");
}
}
运行后果:
从以上代码的运行后果能够看到,SAVE_HOOK 对象的 finalize() 办法的确被垃圾收集器触发过,并且在被收集前胜利自我援救了。
另外一个值得注意的中央是,代码中有两段齐全一样的代码片段,执行后果却是一次自我援救胜利,一次失败了。这是因为任何一个对象的 finalize() 办法只会被零碎主动调用一次,如果对象面临下一次回收,它的 finalize() 办法不会再被执行,因而第二段代码的自救口头失败了。
finalize() 办法是 Java 刚诞生时为了使传统 C、C++ 程序员更容易接受 Java 所做出的一项斗争。它的运行代价昂扬,不确定性大,无奈保障各个对象的调用程序,现在已被官网明确申明为不举荐应用的语法。