对象是否还活着
只有判断对象是否还有引用指向它才能决定是否回收,所以回收的第一步就是判断:“对象是否还活着?”
判断对象的存活状态有两种方式:
引用计数器
有一个引用指向该对象计数器就加 1,一个引用失效计数器就减 1,当且仅当一个对象的计数器为 0 的时候,被下一次 GC 回收。
引用计数器法简单粗暴,但是有一个不足的地方,那就是碰到“相互循环引用”时,引用无法进行回收。
相互循环引用,两个对象的属性中都含有对方的引用,但是除此之外再无其他引用。这样的情况下虽然外界已经无法引用到这两个对象,但是他们的各自的计数器不为 0,不能被回收。
可达性分析算法
该判断一个对象的存活状态的方式可以解决“相互循环引用”的问题。
- 通过创建一组 GCRoot 指针来管理当前被引用的对象,被外界引用的对象 A 就挂在 GCRoot 指针上,如果 A 的属性中引用了对象 B,就将 B 挂在 A 的后面,以此类推,形成树形结构,我们叫做 GCRoot 树(以 GCRoot 为树根的树结构)。
- 如果一个被外部引用的对象跟其他任何一个 GCRoot 树中的节点都没关系,就创建一个新的 GCRoot 指针,组成一棵新的树。
- 如果外界引用减少了一个,就从对应的 GCRoot 树中撤去一条树枝。
- 当一棵树的树根不是 GCRoot 对象的时候,那么就不存在外界的任何一个引用,因此该树上的所有对象都为死亡状态。
垃圾回收是判断一个对象是否存在的原则就是:该对象到 GCRoot 指针是否可达,如下图, Object 5 就是一个不可达的例子,下一次回收的时候会将其回收(包括 Object6 和 Object7)。
对象的引用
一个对象的引用是一个对象存在的根本。只有对引用有了良好的等级划分,才能使得垃圾回收变得“智能”。引用分为四类:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)
强引用
该引用关系最强,代表对象现在富有活力,不能被垃圾回收机制回收,比如 Object strong = new Object();
软引用
只有但内存空间不足的时候,GC 才会将这块内存收回
弱引用
在下一次 GC 时被回收
虚引用
这是最弱的一种关系,不会对对象的生命周期产生任何影响,也无法通过该引用获得对象
finalize方法
一个对象不可达,可以说“行将就木”的时候,并非是“非死不可”,它只是被判了死刑,还没有执行。在这段期间,如果通过 finalize 方法,或许能够让它逃过 GC 的鬼门关
public class OneObject {
private static OneObject hook = null;
...
protected void finalize() {
super.finalize();
// 挽救它
OneObject.hook = this;
}
}
该方法只有被重写并且没有被标记执行过的时候,才有可能执行,否则永远不会被执行。JVM 中会维护一个 F-Queue,所有未被标记的 finalize 方法将会放入该队列中,由一个低优先级的 finalize 线程执行该队列中的方法,被执行的(并不一定执行完)方法将被标记,移除队列。
但是 GC 不会等着该类的 finalize 方法完全执行完才会进行垃圾回收。
原因是:如果该 finalize 方法执行的时间比较长,甚至是一个死循环,那么将会影响其他对象的回收,最终是的 GC 崩溃。
因此如果使用 finalize 方法做一些操作是很不安全的,最好的选择是
try...finally
语句块。
方法区回收
方法区都是一些生命周期比较长的数据,回收一次获得的空闲内存不是很大。主要是回收不使用的常量、无用的类的元信息。
判断一个类还有没有使用相当严格的(因为一个类的加载是很耗资源的),需要满足如下的所有条件:
- 该类的实例全部被回收
- 该类的 ClassLoader 已经被回收
- 该类的 Class 对象没有任何地方引用
垃圾回收算法
确定了那些是“垃圾”,就开始回收吧。
标记清除算法
将无用的对象进行标记,一次性清除所有被标记的对象。
这样的缺点就是产生了大量的碎片,不利于内存管理。但是它是所有回收算法的“鼻祖”,其他的算法大多是对它的一个改进。
复制算法
将内存划分为相等的两块,一块使用,另一块备用。当使用块用完了,将使用块中的 Active 对象规规整整复制到另一块中,清空使用块。这样往复使用。
这样就造成了 4G 内存,实际只有 2G 了,代价太大。但是不会产生内存碎片的问题。
针对代价巨大的缺点,有许多改进的方案。
因为大多数对象的生命周期都比较短,所以将内存分为 3 块(一大两小,大的叫做 Eden,小的叫做 Survivor)。
使用的时候使用 Eden 和一个 Survivor,复制的时候将 Eden 和 Survivor 中的 Active 对象复制到另外一个 Survivor 中,然后进行清除。
标记整理
将零碎的 Active 对象整理成规整的一块内存,清除最后一个对象存储块后面的所有数据。
分代收集
根据对象的生命周期不同(新生代、老年代),将内存划分为块。针对不同“代”使用不同的收集策略。比如新生代的生命周期短,就用复制算法;老年代的生命周期长,就用标记整理算法。