垃圾回收需要完成的三件事情:
3.2 对象已死?
本小节解决垃圾回收机制需要完成的第一件事情“哪些内存需要回收”,根据小节名称可以看出,“死掉”的对象需要被会后,那么我们如何判断对象是死掉的呢,让我们接着向下看!
(2.1). 引用计数算法
★ 实现原理:
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可能再被使用的,即称该对象已经死亡。
★ 优点:
效率高,原理简单。
★ 缺点:
该算法有很多例外情况要考虑, 必须要配合大量额外处理才能保证正确地工作, 譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
我们采用书中的例子来看一下我们HotSpot的虚拟机是不是采用的引用计数
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
public static void main(String[] args) {
ReferenceCountingGC.testGC();
}
}
GC日志打印如下图所示: 如图所示,*[PSYoungGen: 7567K->840K(67072K)]*代表我们的垃圾收集器并不受循环引用的影响。这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。
(2.2)可达性分析算法
★ 实现原理:
通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) , 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。
在Java技术体系里面, 固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表) 中引用的对象, 譬如各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等
- 在方法区中类静态属性引用的对象, 譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象, 譬如字符串常量池(String Table) 里的引用
- 在本地方法栈中JNI(即通常所说的Native方法) 引用的对象。
- Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
- 所有被同步锁(synchronized关键字) 持有的对象。
- 反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等。
除了这些固定的GC Roots集合以外, 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同, 还可以有其他对象“临时性”地加入, 共同构成完整GC Roots集合。
(2.3)再谈引用
引用的定义:如reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称该reference数据是代表某块内存、 某个对象的引用。
采用如上描述的引用定义,则只会出现“被引用”或者“未被引用”两种状态,对于描述一些“食之无味, 弃之可惜”的对象就显得无能为力。 譬如我们希望能描述一类对象: 当内存空间还足够时, 能保留在内存之中, 如果内存空间在进行垃圾收集后仍然非常紧张, 那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应用场景。jdk1.2之后对此种情况作了补充。
- Strongly Re-ference: 强引用是最传统的“引用”的定义, 是指在程序代码之中普遍存在的引用赋值, 即类似“Object
obj=new Object()”这种引用关系。 无论任何情况下, 只要强引用关系还存在, 垃圾收集器就永远不会回收掉被引用的对象。 - Soft Reference:软引用是用来描述一些还有用, 但非必须的对象。 只被软引用关联着的对象, 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围之中进行第二次回收, 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- Weak Reference:弱引用也是用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止。 当垃圾收集器开始工作, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。
- Phantom Reference:虚引用也称为“幽灵引用”或者“幻影引用”, 它是最弱的一种引用关系。 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
(2.4)生存还是死亡
要真正宣告一个对象死亡, 至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记, 随后进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。 假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必要执行”。
由此我们可以在finalize中将引用重新赋值GCRoot = this 使得此对象复活,但是finalize方法只可被虚拟机调用一次,也就是说只有一次复活的机会,但是这种做法非常危险,一旦finalize方法被阻塞,将会导致虚拟机的F-Queue 队列的其它finalize方法无法执行造成内存回收子系统的崩溃。
作者对此如是说: finalize()能做的所有工作, 使用try-finally或者其他方式都可以做得更好、更及时, 所以笔者建议大家完全可以忘掉Java语言里面的这个方法。
(2.5)回收方法区
前面我们知道堆是GC的重点关照对象,但是jdk1.8之后的HotSpot虚拟机中元空间(Metaspace)取代了永久代(PermGen)成为方法区的具体实现,由此方法区开始接受GC的管理。但是由于方法区存储内容的特殊性,导致了它有相当苛刻的GC条件:
- 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGi、 JSP的重加载等, 否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。
|