生存还是死亡??

在文章的开头,引用《哈姆雷特》中的一句话「TO BE OR NOT TO BE - 生存还是毁灭」

 

在文章 运行时数据区域 中介绍过:「程序计数器、虚拟机栈、本地方法栈」三个区域是线程私有滴,随线程而生,随线程而灭;而「堆、方法区」是线程共享滴!

栈中的栈帧随着方法的进入和退出而有条不紊地执行着入栈和出栈的操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已经确定

所以「程序计数器、虚拟机栈、本地方法栈」三个区域的内存分配和回收具备确定性,当方法结束或者线程结束时,内存自然就跟着回收了

而「堆、方法区」两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不太一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间才能知道程序究竟需要哪些对象,创建多少个对象,这部分内存的分配和回收是动态的

垃圾收集是 Java 中不可或缺的一部分,它所关注的正是「堆、方法区」部分内存的管理问题!!!总的来说,它需要完成三件事情:

而本篇文章主要来解决第一件事情:哪些内存需要回收?

堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还「存活」着,哪些已经「死去」

这里说的是几乎,是为了和《深入理解 Java 虚拟机》作者的观点保持一致,但我们更应该用辩证的态度去看待这个观点,关于这个观点更多分析可见 逃逸分析

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值 +1;当引用失效时,计数器值 -1;任何时刻计数器值为 0 的对象就是不可能再被使用的

上面所说的是利用「引用计数」来判断对象是否存活的算法核心思想,它有一些好处:原理简单,判断效率高

但 Java 领域并没有选择使用它来管理内存,原因:存在对象间相互循环引用的问题

举个简单的例子,如下图所示:

22

对于上图的右边,实际上这两个对象已经不可能再被访问,但由于它们俩互相引用着对方,导致它们的引用计数器值都不为 0,引用计数算法也就无法回收它们!!

可达性分析算法

假设我们已经知道了一定不可能被回收的对象集合,那么与这个集合中有关联的对象也一定不能被回收,与这个集合中无关联的对象就一定可以被回收,就算它们之间存在相互循环引用

按照这个思路,给出更加规范的定义:通过一系列称为「GC Roots」的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为「引用链」,如果某个对象到 GC Roots 间没有任何引用链相连 (或者用图论的话来说就是从 GC Roots 到这个对象不可达时),则证明此对象是不可能再被使用的

所以总体来说可达性分析算法主要过程包括:枚举根节点 & 查找引用链。更严格的来说,「GC Roots」中应该存放的是一组必须活跃的引用

23

注意:该算法的本质是通过找出所有活对象来把其余空间认定为「无用」,而不是找出所有死掉的对象并回收它们占用的空间

那什么样的对象引用可以放到「GC Roots」集合中呢??

在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:

对于上述第一种可作为 GC Roots 的对象来举一个例子,具体代码如下所示:

首先当执行到f()方法中时,进行一次 heap dump;然后当回到main()方法中时,进行第二次 heap dump

利用 MAT 软件分析两次的 dump 文件,如下图所示:

gcroot

可以看到,第一次 dump 时,user在 GC Roots 集合中;第二次 dump 时,user不在 GC Roots 集合中。从 GC Roots 的数量上也可以看出来,两次 dump 只相差一个user

所以可以得出结论,当执行f()方法时,user会加入到 GC Roots 集合中;当方法执行完,栈帧出栈后user会从 GC Roots 集合中去除

除了这些固定的 GC Roots 集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象「临时性」地加入,共同构成完整的 GC Roots 集合

更多内容可见 垃圾收集算法 中第三点假说!!

例如:在局部回收中,仅仅对 Java 堆中某一块区域发起垃圾收集时 (最典型的是只对「新生代」的垃圾收集),如果该区域的对象被堆中其他区域的对象所引用,这个时候就需要将关联区域的对象也一并加入 GC Roots 集合中去,才能保证可达性分析的正确性。换句话说:在执行局部回收时,从 GC 堆的非回收部分指向回收部分的引用,也必须作为 GC Roots 的一部分

55

如上图所示,如果此时对新生代进行垃圾收集,那么Object5也必须作为 GC Roots 的一部分!

注:当前主流的商用程序语言 (Java,C#,Lisp) 的内存管理子系统都是通过「可达性分析算法」来判定对象是否存活的!!

对象引用

无论是「引用计数算法」,还是「可达性分析算法」,它们判断对象的存活都离不开「引用」

在 JDK 1.2 之前,Java 里面的引用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存的地址,就称该 reference 数据是代表某块内存、某个对象的引用

这个定义在现在看来过于狭隘,一个对象在这种定义下只有两种状态:「被引用」or「未被引用」

我们希望对于一个对象:当内存空间还足够时,能保留在内存之中;如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这个对象

基于上述所描述的应用场景,在 JDK 1.2 之后,Java 对「引用」的概念进行了扩充,分为:

关于上述四种引用更详细的内容可见「强/软/弱/虚」引用

生存还是死亡

终于回归这篇文章的初心了,本来只是想简简单单总结一下finalize()方法的作用,但是如果仅仅介绍这个方法,又感觉前不着村,后不着店,没头没尾的

所以秉着系统化,完整化,结构化的原则,还是稍微总结了一下与之相关的基础知识,不说废话了,现在开始!!

即使在可达性分析算法中判定为不可达的对象,也不是「非死不可」的,这个时候它们暂时还处于「缓刑」阶段,真正要宣告一个对象死亡,最多会经历两次标记过程,如下图所示:

44

根据上面的流程可以看出一些finalize()方法的特点:当没有任何引用指向一个对象时,按理来说它应该被回收,但垃圾回收器会给它一次自救的机会,在回收前会执行该对象的finalize()方法!!

正是因为该机制使得一个对象并非只有两种状态:存活 or 死亡;还有一种中间状态:可复活态 (最多只有一次复活机会)

因为finalize()方法的特点:在回收前会被执行一次,导致被用来「关闭外部资源」之类的清理工作,但并不建议使用!原因:

下面给出一段演示对象复活的代码:

回收方法区

《Java 虚拟机规范》中提到可以不要求虚拟机实现对方法区的垃圾收集,也确实有虚拟机未实现

相比于「堆」中的垃圾收集来说,「方法区」的垃圾收集性价比较低,尤其是在新生代区域中,一次垃圾收集通常可以回收 70% - 99% 的内存空间

方法区的垃圾收集主要回收两部分内容:「废弃的常量」「不再使用的类型」

回收「废弃的常量」和回收 Java 堆中的对象非常类似!

判断一个常量是否「废弃」还是相对简单,而要判断一个类型是否属于「不再被使用的类」的条件就比较苛刻,需要同时满足以下三个条件:

Java 虚拟机「被允许」对满足上述三个条件的无用类进行回收,这里说的仅仅是「被允许」,而不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,可以通过参数控制

在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 Java 虚拟机具备「类型卸载能力」,以保证不会对方法区造成过大的内存压力

参考文章