GC需要完成的3件事情
哪些内存需要回收?
在隔离区的程序计数器、虚拟机栈、本地方法栈三个区域随着线程而生而灭。每一个栈帧中分配多少内存基本上都是固定已知的,因此这几个区域的内存分配和回收具有确定性,就不多考虑内存回收的问题了,在方法或线程结束的时候就自然回收了这部分的内存。在java堆和方法区中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建那些对象,这部分内存的分配和回收是动态的,GC所关注的就是这部分内存。
什么时候回收?
回收第一件事要搞明白就是,在堆中,哪些对象时“存活”的,哪些“死去”的不再被任何途径使用的对象。两种存活判断方法:引用计数算法、可达性分析算法;
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被引用的。弊端:不能解决对象间相互循环引用的问题,所以java虚拟机不采用。
采用的一般有:微软的COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言等进行内存管理。
可达性分析算法:通过一系列的称为“GC roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain,简称RC),当一个对象到GC Roots没有任何RC相连,证明此对象时不可用的。现在主流的商用语言(java、C#等)都是使用可达性分析算法。
在java语言中,可作为GC Roots的对象包含有
虚拟机栈中的引用对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象;
生存还是死亡?
即使在可达性分析算法中不可达的对象,也并非是必须被回收,可以缓一缓,对象可以进行一次自救。在垃圾回收清理内存之前先调用一次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{
super.finalize();
System.out.println("finalize method executed!");
//对象重新建立 GC root 连接链
FinalizeEscapeGC.SAVE_HOOK=this;
}
public static void main(String[] args) throws Throwable{
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一一次成功拯救自己;
SAVE_HOOK =null;
System.gc();
//因为fianlize方法优先级很低,所以暂停0.5秒等待他
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,i am dead :(");
}
//下面这段代码与上面的完全相同,但是这次自救却失败了;-------------------finalize方法只会被调用一次;
SAVE_HOOK =null;
System.gc();
//因为fianlize方法优先级很低,所以暂停0.5秒等待他
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,i am dead :(");
}
}
}
对象自救描述:
1、判断是否存在对象与GC Root的reference chain:
如果引用链存在:回收内存;
2、如果不存在:第一次进行标记,并且进行筛选(筛选条件:是否有必要执行finalize()):
没必要执行:当对象没有覆盖finalize()或finalize()方法已经被调用过一次,回收内存;
3、有必要执行:将对象放到F-Queue的队列中,自动创建线程finalizer,第二次小规模标记;
finalize()未执行完,自救失败:回收内存
4、建立引用链,自救成功:不回收
如何回收?
垃圾收集算法
标记-清除算法(mark-Sweep)
不足:两个过程,效率不高;
清除之后会产生大量不连续空间碎片,从而导致在程序运行时,无法找到足够连续内存,不容易给大对象分配空间,而出发另一次垃圾收集动作。
标记-清除算法是算法的基础,因为这个算法有以上不足,以下算法都是针对这些特点进行修改和优化。
复制算法(copying)
不足:对象存活率较高时,频繁进行复制操作,效率变低;如果不想浪费50%的空间,就必须额外的空间进行分配担保;
标记-整理算法(Mark-Compact)
和标记-清理算法标记过程相同,但不会直接清理掉对象,让所有存活的对象向一端移动,直接清理掉端边界外的内存。
分代收集算法
将java堆分为新生代和老年代,新生代根据其特点,使用复制算法,只需付出少量存活对象复制的成本,而老年代特点是对象存活率时间长,没有额外空间做担保,只能选择标记-清理或标记整理