垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象中哪些还“存活”,哪些已经“死去”。
引用的分类
Java将引用分为4种:
-
强引用:指在程序代码之中普遍存在的引用赋值,即类似 “Object obj = new Object ()”
这种引用关系。无论何种情况下,只要强引用关系存在,垃圾收集器就不会回收调被引用的对象。 -
软引用:用来描述一些还有用,但非必须的对象。只要被软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
-
弱引用:也是用来描述那些非必须对象,但它的强度比软引用还弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生为止
-
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器值就减一;计数器为零的对象就是不可能再被使用的。
然而,在Java中,并没有选择引用计数法来进行内存管理。
可达性分析算法
可达性分析算法的基本思路就是通过一系列称为“GC Roots” 的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索走过的路径称为“引用链”。如果某个对象到GC Roots 之间没有任何引用链,则证明该对象是不可能被使用的。
可作为GC Roots的对象如下:
- 在虚拟机栈中引用的对象,比如各个线程被调用方法堆栈中的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,比如引用类型静态变量
- 在方法区中常量引用的对象,比如字符串常量池里的引用
- 在本地方法栈中JNI(Native)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,常驻的异常对象
- 所有被同步锁(synchronized)持有的对象
- 反映虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了以上外,也可以"临时"加入一些对象作为GC Roots对象。(比如在新生代回收时,就需要考虑老年代对象的调用,需要将关联区域的对象也一并加入到GC Roots中)
死亡逃脱
先看以下代码
/**
* 1.对象可以在被GC时自我拯救
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("I am still alive !");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
// 这里自救,重新建立关联
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable{
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为finalizer 方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("I am dead!");
}
//下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为finalizer 方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("I am dead!");
}
}
}
执行结果:
这是为什么呢?
第一次成功自救是因为真正对一个对象回收,需要进行两次标记。
如果对象在进行可达性分析后,发现没有引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是是否有必要执行finalizer 方法,假如对象没有没有覆盖finalizer ()方法,或者finalizer ()已经被调用过,那虚拟机将这两种情况视为没必要执行。第二次不能逃脱,是因为系统已经执行过finalizer方法了
如果对象被判断为有必要执行finalizer()方法,那么该对象会被放进一个低优先级的队列F-Queue中,稍后收集器将对F-Queue中的对象进行第二次小规模的标记。第一次逃脱就是因为在finalizer方法中重新建立了关联,所以第二次标记时它被移出"即将回收" 的集合。
finalizer方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,已经被官方声明为不推荐使用的语法。笔者建议大家使用try-finally方式
回收方法区
方法区的垃圾收集主要回收两部分内容: 废弃的常量和不再使用的类型
参考:《深入理解Java虚拟机》