垃圾收集
- 哪些内存需要回收?
- 什么时候回收?
- 怎么回收?
为什么需要了解垃圾收集呢?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
由于栈是归属于每个独立线程的,因此当线程结束或方法结束时,内存就自然被回收,而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,而垃圾回收器所关注的就是这部分内存。
确定对象存活状态
垃圾收集器在对堆进行回收前,需要确定堆中哪些对象还“活着”
引用计数算法
常规的思路:给对象中添加一个计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
然而主流的JVM并没有选用这种算法来管理内存,因为很难解决对象之间相互循环引用的问题。例如:
public class GC{
public Object instance = null;
public static void testGC(){
GC a = new GC();
GC b = new GC();
a.instance = b;
b.instance = a;
System.gc();//系统的回收方法
}
}
实际上,在调用了System.gc()后,a和b都可以被回收,这也侧面说明了JVM不是采用的引用计数器算法来判定对象的存活状态的。
可达性分析算法
在主流的商用程序语言(JAVA、C#)的主流实现中,都是通过可达性分析来判断对象是否存活。
基本思路:通过一系列的“GC Roots”的对象作为起点,从这些起点向下搜索,如果有对象无法连接到GC Roots集合中的任何一个根的话,则该对象被判定为不可用。
在JAVA中,可作为GC Roots的对象包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
生存还是死亡
即使在可达性分析中不可达的对象,也并非一定要被回收,真正回收一个对象,需要经过两次标记过程。
- 判断GC Roots是否可达
- 判断是否有必要执行finalize()方法
如果finalize之前已经被调用过或者当前对象并没有重写该方法,则没有必要执行,直接被回收;如果被覆盖,则该对象会被放在一个F-Queue中,并在之后由一个优先级较低的Finalizer线程去执行它。
如果在执行finalize()的过程中,改对象能够重新与引用连接上,那么改对象就自救成功,不会被回收了。
这里要注意的是:finalize只有一次自救机会,一旦被调用过之后,下一次再第二次标记时就会被回收。
来看这段代码:
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 Throwable {
// TODO Auto-generated method stub
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5s等候他
Thread.sleep(500);
if(SAVE_HOOK!=null) {
SAVE_HOOK.isAlive();
}else
System.out.println("no, i am dead :(");
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5s等候他
Thread.sleep(500);
if(SAVE_HOOK!=null) {
SAVE_HOOK.isAlive();
}else
System.out.println("no, i am dead :(");
}
}
结果是
finalize method executed!
yes,i am still alive :)
no, i am dead :(
可以看出,SAVE_HOOK对象的finalize确实被执行过,并且成功自救了;同时也说明了finalize调用过一次之后,就无法再进行自救。
回收方法区
方法区中的内存回收主要包括两个部分:废弃常量和无用的类
废弃常量的回收与堆中的对象回收非常相似。只要该常量在当前系统中没有任何变量对其引用,则进行回收。
无用类的判定条件则相对苛刻:
- 该类的所有实例都已经被回收——堆中不存在任何该类产生的对象
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
总的来说,在堆中进行一次垃圾收集一般可以回收70%-95%的空间,而对方法区的回收效率却远低于此。
网友评论