浅谈java垃圾收集器
前几天面试,被问到谈下java是怎样回收垃圾的,当时回答得太少,所以回去自己看了下java虚拟机是如何管理这一块的。
分享下自己的看法,欢迎大家和我一起探讨。
java不像c++一样,java是把垃圾回收完完全全的交给了虚拟机去处理,C++则需要自己的分配内存以及回收垃圾。当然java这一做法极大的降低的编程的难度,但是其中也有一些弊端。
我在这里主要探讨下java是如何回收堆区和方法区的内存(垃圾)回收吧,后面说的“内存”都值这部分内容。
垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些以及死去(即不可能再被任何途径使用的对象)。
但是,怎样判断一个对象是否存活呢?我当时答的是:
引用计数算法,给对象添加一个引用计数器,每当一个地方引用它时,计数器值就加一;
当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不可能再被使用的。
当时面试官听到后,是这样说的,引用计数算法,判断效率很高,在大部分情况下它都是一个不错的算法,应用也比较广泛。
我以为我的答案还让面试官比较满意,结果回去自己看了一下,
主流的java虚拟机里面它是没!有!选用引用计数算法来管理内存的,最主要的原因就是它很难解决对象相互循环引用的问题!(就像两个对象的实例相互指向,当着两个的引用为null,由于他们还是相互引用着对方,导致他们计数器都不为0,于是无法回收。)
那虚拟机是怎样来判断对象死了呢?
可达性分析算法,这个算法基本思路就是通过一系列的称为"GC Roots "的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象的不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中Native引用的对象。
但是!!!!!即使是在可达性分析算法中不可达的对象,也并非是“非死不可”的。
这时候它们只是暂时处于“缓刑”阶段,要真正宣告一个对象死亡时,至少要经历两次标记过程:
第一次是进行可达性分析后发现没有引用链,则进行第一次标记,标记之后则会进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,则这个对象死亡,可以回收。
但是,当这个对象判定为有必要执行finalize()方法时,那么这个对象将会放置一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行它。这里的执行意思是,虚拟机会触发这个方法,但并不承诺会等待他运行结束。这样做的原因是,如果一个对象的finalize()方法中执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中其他对象永久处于等待,甚至整个内存回收系统崩溃!
finalize()方法是对象逃脱被回收的最后一次机会,稍后GC将对F- Queue中的对象进行对二次小规模的标记,如果对象要在finalize()中逃脱,只有重新将引用链上的任何对象建立关联即可,譬如把自己(this关键字)赋给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果没有逃脱,则被回收掉。
但是,java不建议程序员这样去做,这并不是c/c++的析构函数,只是java刚诞生时为了是C/C++程序员更容易去接受所做出的妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好更及时。所有不建议用这个方法。
了解了堆区的回收之后,再来了解方法区的回收就要简单多了。
虽然方法区的垃圾收集效率远低于堆区,但是方法区依旧会内存溢出。这里主要回收两部分内容:
废弃常量和无用的类。
废弃常量的回收与回收java堆中的对象非常相似。以常量池中字符串“abc”为例。加入一个常量池中存在字符串“abc”,但是当前系统没有一个String对象是叫做“abc”的,这是就会发生内存回收,而且有必要的话,这个“abc”常量将会被系清理出常量池。常量池中的其他类(接口),方法,字段的符号引用也与此相似。
那,无用的类是怎么样回收的呢?它的条件就苛刻很多了。
类需要同时满足下面三个条件才能算是“无用的类”:
1.该类所有的实例都已经被回收
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Classs对象没有在任何对方被引用,无法在任何地方通过反射访问该类
虚拟机可以对满足上述三个条件的无用类进行回收,但只是“可以”,并不像对象,不使用了必然回收。虚拟机提供了一些参数进行控制,可以查看类加载和卸载信息。
在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP已经OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
至于,垃圾回收的算法,整理好了之后在于大家探讨。