深入理解JVM之二:垃圾收集器概述
前言我们知道Java的内存区域分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区,而且其中的程序计数器、虚拟机栈和本地方法栈都是线程独立的,也就是说这三块内存区域的生命周期与线程是同生共死的。栈中帧栈在类结构确定的时候就已经知道该分配多少内存了,所以当线程结束的时候,内存也跟着一起回收了,从这个角度看,这三块的内存区域的内存分配和垃圾收集就比较固定了。反观Java堆和方法区,比如我们定义一个
前言
我们知道Java的内存区域分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区,而且其中的程序计数器、虚拟机栈和本地方法栈都是线程独立的,也就是说这三块内存区域的生命周期与线程是同生共死的。栈中帧栈在类结构确定的时候就已经知道该分配多少内存了,所以当线程结束的时候,内存也跟着一起回收了,从这个角度看,这三块的内存区域的内存分配和垃圾收集就比较固定了。反观Java堆和方法区,比如我们定义一个接口,接口有着不同的实现类,而每个实现类的内存可能会不一样,每个实现类的方法的多个语句分支也可能需要的内存不一样,诸如此类。所以这两块区域的内存分配具有不确定性,那么在垃圾回收的时候自然也存在不确定性。在Java的垃圾收集机制中,关注的是这两块内存区域的垃圾回收。
为什么要垃圾回收
在接触Java虚拟机之前,只听过什么老年代和年轻代。在接触Java虚拟机之后,才知道所谓的年轻代和老年代只是垃圾回收中的一种分代回收算法(后面还会介绍)。现在我们比较关心的是为什么要进行垃圾回收?没有垃圾回收不行吗?想必了解C语言的伙伴知道,C中有malloc、free等于内存分配以及内存释放的函数,这又是为什么呢?我们知道电脑的内存是有限的,如果一段程序申请了一块内存空间并执行完计算之后,没有释放内存,会导致这块内存被占用,那么可用内存就变少了,那么问题来了,如果一个系统很庞大,程序中迟早会把电脑内存耗尽的。为了提高内存的使用效率,内存在使用完必须释放,这样其他程序才可能重新申请这块内存。
可达性分析算法
好了,接下来我们释放了内存空间,一块内存中有许多对象,这些对象是有生命周期的,当生命周期终结的时候,垃圾回收器就起作用了。然而问题又来了,如何判断一个对象的生命周期已经终结呢?火换句话说就是计算机是如何判断一个对象已经死亡了呢?在JVM领域,就有了垃圾回收算法,不过在Java虚拟机的垃圾回收算法出现之前,曾出现过引用计数算法和可达性分析算法,前者大家可以自行Google,这里主要介绍可达性分析算法。
之所以介绍这个算法,是因为这个算法是Java虚拟机中所借鉴的,自然有必要了解一二。
算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起始点(什么对象能够作为GC Roots,后面还会介绍),从这些起始点开始向下搜索,所走过的路径称为引用链,如果一个对象到GC Roots没有任何引用链,那么这个对象是不可用的,就是说,程序中没有谁引用了这个对象,所以可以说从根节点到叶子结点是不可达的(学过图的应该知道)。
下面的问题是什么对象能够作为“GC Roots”对象呢?Java中,以下对象可作为GC Roots对象:
- 虚拟机栈(栈帧中本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(也就是native本地方法)引用的对象
OK,现在我们已经知道了通过可达性分析判断对象是否还可用,但是是不是不可达的对象都是不可用呢?答案是未必。原因在于要宣告一个对象的死亡,需要两次标记(为什么需要两次标记呢?原因是如果一个对象没有与GC Roots结点相连,就会被第一次标记,而如果对象覆盖了finalize方法,并且在finalize方法中与某个对象建立了引用关系,那么第二次标记会失败,那么这个对象就会被移出“即将回收”的对象列表,移出之后这个对象就“活”了下来,如果在finalize方法中这个对相关仍然没有与一个对象建立引用关系,那么这个对象就真正死亡了)。
下面的这段代码可以说明这个问题:
代码清单:
/**
* Test finalize method wwhether can prevent an object from GC
* @author Administrator
*
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOk = null;
public void isAlive(){
System.out.println("yes,i am still alive");
}
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOk = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOk = new FinalizeEscapeGC();
//Object SAVE_HOOK has a reference so it will survive
SAVE_HOOk = null;
System.gc();
//because the finalize method has a low priority,
//it will be helpful to ensure finalize method is actually executed
Thread.sleep(500);
if(SAVE_HOOk != null){
SAVE_HOOk.isAlive();
}else{
System.out.println("no,i am dead!");
}
/************The two parts is the same*******************/
SAVE_HOOk = null;
System.gc();
//because the finalize method has a low priority,
//it will be helpful to ensure finalize method is actually executed
Thread.sleep(500);
if(SAVE_HOOk != null){
SAVE_HOOk.isAlive();
}else{
System.out.println("no,i am dead!");
}
}
}
最后的输出结果是这样的:
从上面的测试代码中可以知道,即使在GC Roots中的没有引用链,并且被第一次标记,那么只要在finalize方法中是这个对象添加一个引用就可以。这样对象就能够逃脱“被判死刑”的命运了。然而,这种方式并不是官方推荐的,因为finalize方法运行运行代价高昂,不确定大,无法保证各个对象的调用顺序
,在Java中try-finally块都比finalize方法好。
回收永久代对象
前面说了,Java的内存回收主要实在方法区和Java堆中,Java堆中的新生代(对象的分代将在下一篇博客中说明),因为新生代的存活时间比较短,所以对新生代进行垃圾回收回收的空间比较大,但是方法区中的永久代则由于可能存活时间较长,所以下一次的垃圾回收回收该对象的可能性没有新生代那么大。所以对永久代的回收效率会大打折扣。但是这部分对象仍然是需要回收。
永久代的垃圾回收包括两部分:废弃常量和无用的类
废弃常量的回收比较好理解,因为只要没有任何对象引用常量池中的某个对象,那么这个对象就会被回收(等等,之前不是说一个对象如果要宣判“死刑”,必须需要两次标记吗,怎么现在一次就够了呢?主要在于前面说的是非常量池中的对象,废弃常量回收的是运行时常量池中的对象,所以只需要一次标记就好)。
那么无用的类又应该如何回收呢?
需要满足以下三个条件才可以宣判一个类的“死刑”:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
注意上面的可以,仅仅是可以哦。所以与对象的回收不同,是否需要对类进行回收,需要设置相关的参数才行。
更多推荐
所有评论(0)