1.序言
在Java堆的世界里,存放着几乎所有的对象实例,所以垃圾回收的主要对象就是堆。而进行回收前,第一件事就是要确定这些对象那些还活着,哪些已经“死去”。而能做这件事的判官主要有两种,一种是引用计数算法,另外一种就是可达性分析算法,当然,判断在判定生死的时候还会给一张仅仅能使用一次的免死金牌(这个待会会说)。接下来让我带大家进入两种算法的真面目,接下来看一下那张免死金牌究竟张啥样。
2.引用计数算法
2.1 概念
对象出生之际都会附带一个引用计数器,每当一个地方对他引用,计数器就会+1;当引用失去,计数器就会-1;任何时刻计数器为0的状态,他就不可能被引用。此时垃圾回收期就会把计数器为0的对象回收。
2.2 特点
1)优点: 处理简单,判定效率高。
2)缺点: 不能解决循环引用的问题。下面提供一段循环引用的代码,大家可以琢磨一下。
public class Box{
private static final int size = 1024 * 1024;
private byte[] bigSize = new Byte[2 * size];
private Box instance;
public static void testGC(){
Box redBox = new Box();
Box blueBox = new Box();
redBox.instance = blueBox;
blueBox.instance = redBox;
redBox = null;
blueBox = null;
System.gc();
}
}
2.3 应用案例
微软公司的Component Object Model技术、ActionScript3的FlashPlayer、Pyhton语言、Squirrel。看到没有,至少Java就没有使用这种算法来管理内存。
3.可达性分析
3.1 准备工作
在进行分析可达性分析之前,我感觉有必要先简单普及一下对象内存布局的三块区域,对象头(Header)、实例数据(Instance Data)和对齐填充。
3.1.1对象头
对象头有32位组成,前25bit存放对象哈希码,4bit用于存放对象分代年龄,2bit用于存放锁标志位,1bit固定为0.
存储内容 | 标志位 | 状态 |
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级所的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
3.1.2实例数据
这部分真正存放对象实例有效数据,包括父类继承下来的和子类中定义的。这部分的存储顺序会受到虚拟机分配策略和字段在Java源码定位的顺序决定的。
3.1.3 对齐填充
通过名字就可以看出来这一部分只是填充使用,并没有实际作用。
3.1 基本概念
通过一系列称之为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GCRoots没有任何引用链的时候,则证明此对象不可用。收集器对于引用链上的对象会在对象头做标记,正如上面对象头分布所示。下图中,object4-object6没有到GCRoots相连,也就是被回收的对象。
3.2 作为GCRoots的对象
3.1 虚拟机栈(栈帧中的本地变量表)中引用的对象。
3.2 方法区中类静态属性引用的对象。
3.3 方法区中常量引用的对象。
3.4 本地方法栈中JNI(即一般说的Native方法)引用的对象。
3.3 引用类型
在Jdk1.2后,Java对引用的概念进行了扩充,分为四种方式引用。
类型 | 特点 | 触发时机 |
强引用(Strong Reference) | 引用还在,就一直活着 | 不会触发(初非不可达) |
软引用(Soft Reference) | 一些有用但是非必须的对象 | 当系统要发生内存溢出之前,会对软引用进行二次回收,如果内存还不够,才会发生内存溢出。 |
弱引用(Weak Reference) | 非必须对象 | 不管他处于何种状况,主要发生GC,他就会被清除。 |
虚引用(Phantom Reference) | 成为幽灵引用或幻影引用 | 一个对象是否虚引用,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。他的目的是能在这个对象呗收集器回收时收到一个系统通知。 |
4.免死金牌
要宣布一个对象死亡至少要经历两次标记过程。如果对象在进行可达性分析后发现没有GCRoots相连接,他会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有重写此方法或者已经被调用过,那么虚拟机将这两种情况都视为没有必要执行。如果有必要执行,那么这个对象会放置在一个叫做F-Queue列队中,并稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行他。可以看一下下面代码。
public class FinalizeEscapeGC{
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes,I'm 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 = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE.HOOK.isAlive();
}else{
System.out.println("no,I'm dead");
}
//下面代码和上面完全一致
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE.HOOK.isAlive();
}else{
System.out.println("no,I'm dead");
}
}
}
输出结果:
finalize method executed!
yes,I'm still alive
no,I'm dead
5 回收方法区
5.1 永久代在垃圾收集主要回收两部分内容:废弃常量和无用类。
5.2 判断一个类是否无用的条件
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
5.3使用场景
在大量使用反射、动态代理、CGLib等框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载类的功能,以保证永久代不会被溢出。