引言
上一篇文章 JVM 基本介绍 我们了解了一些基本的 JVM 知识,本篇开始逐步学习垃圾回收,我们都知道既然叫垃圾回收,那回收的就应该是垃圾,可是我们怎么知道哪些对象是垃圾呢? 哪些对象需要被回收? 什么时候需要回收呢?
判断算法
引用计数算法
给每个对象设置一个计数器,每当该对象被引用时引用计数器加 1,有引用断开时引用计数减 1。当引用计数为 0 时表示该对象可以被回收。
这个可以用数据算法中的图形表示,对象 A-对象 B-对象 C 都有引用,所以不会被回收,对象 B 由于没有被引用,没有路径可以达到对象 B,对象 B 的引用计数就就是 0,对象 B 就会被回收。
优点
客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软公司的 COM(Component Object Model)技术、使用 ActionScript 3 的 FlashPlayer、Python 语言和在游戏脚本领域被广泛应用的 Squirrel 中都使用了引用计数算法进行内存管理。
缺点
主流的 Java 虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。因为相互引用,计数器值永远也不会成为 0 ,所以永远达不到被 GC 回收的条件。
例如下图:对象A,对象B 循环引用,没有其他的对象引用A和B,则A和B 都不会被回收。
可达性分析算法
该算法的原理是:以 GC Roots
的对象作为起始点,然后以该节点为基准开始向下搜索,搜索过程中搜索路径我们称之为引用链,当一个对象到 GC Roots
没有任何引用链连接的时候,说明该对象是不可用的。
注意:我们在 上边的所说的引用都是指定的强引用关系。
因为我们知道 Java 中存在四种引用对象,根据引用强度(从上至下依次减弱)可依次划分为:
- 强引用
Strong Reference
- 软引用
Weak Reference
- 弱引用
Phantom Reference
- 虚引用
Soft Reference
详细的概念大家下去可以自行查看,此处不再赘述。
可以用作 GC Roots 的对象
- 方法区 : 类静态变量引用的对象
- 方法区 : 常量引用的对象
- 虚拟机栈 : 本地变量表中引用的对象
- 本地方法栈 : JNI (带 Native 关键字)引用的对象
如下图,对象 D 和根对象之间毫无引用链,则会被回收。
由于这种算法即使存在互相引用的对象,但如果这两个对象无法访问到根对象,还是会被回收。如下图:对象 C 和对象 D 互相引用,但是由于无法访问根节点,还是会被回收。
不可达是不是就一定会被回收?
答案是不一定。
一个对象在真正被回收之前,需要经历两次标记过程:
第一次标记:
如果对象在进行可达性分析之后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,此时分两种情况:
-
对象没有覆盖 finalize() 方法
-
finalize() 方法已经被虚拟机调用过
在这两种情况下虚拟机都认为此时没有必要执行垃圾回收。
第二次标记:
在第一次标记判定基础之上,如果判定为有必要执行 finalize() 方法,则虚拟机会把这个对象放置到一个叫做 F-Queue 的队列之中,并在之后用 Finalizer 线程去执行回收。(此处的执行指的是 虚拟机会去触发这个方法,但是并不保证回收成功,也不承诺会等待他运行结束,因为如果有个别对象在 finalize() 方法中执行缓慢甚至发生死循环的时候,有可能会导致 F-Queue 队列中的其他对象发生永久等待,最后导致整个垃圾回收系统崩溃)
总结
- 引用计数法:简单高效,但是对于相互循环引用的对象无法判断是否应该被回收
- 可达性分析:目前大多虚拟机厂商采用的垃圾回收算法,通过判断其他对象是否和根节点之间存在引用链来分析是否应该被回收
- 不可达的对象不一定会立即被回收