JVM线程私有部分(程序计数器,虚拟机栈,方法栈)随着线程而生,随线程而灭,内存分配和回收都具有确定性,而堆、方法区(jdk8为元数据区),内存分配和回收都是动态的。 GC做的三件事
- 哪些内存需要回收(判断对象存活)
- 如何回收(回收算法)
- 谁去回收
一、判断对象存活
引用计数算法
引用计数算法很简单,类似重入锁原理。与对象它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数+1,如果删除对该对象的引用,那么它的引用计数就-1,当该对象的引用计数为0时,那么该对象就会被回收。
主流java虚拟机并未采用该算法,很难解决对象之间相互循环引用的问题,如:对象A指向B,对象B反过来指向A,此时它们的引用计数器都不为0,但它们俩实际上已经没有意义因为没有任何地方指向它们。
例如:
class A {
private B b;
//get set方法
}
class B {
private A a;
//get set方法
}
public class Test {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
}
}
可达性算法
逐个检查,势必消耗很多时间
通过可达性分析(Reachability Analysis)来判定对象是否存活的。通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
可作为GC Root的对象:这也是一个面试高频题
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
2、对象自救:
Java虚拟机通过可达性分析判断对象引用不可达需要被回收时,不是立即被回收,还需要经历一次标记和一次筛选,筛选的条件是此对象是否需要执行finalize()方法。
条件为:当对象覆盖finalize()方法,且该对象的finalize()方法没有被虚拟机执行过。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK=null;
public void isAlive()
{
System.out.println("still alive");
}
//重写finalize方法 使得本对象重新被变量引用
@Override
protected void finalize()throws Throwable
{
super.finalize();
System.out.println("finalize method execute");
FinalizeEscapeGC.SAVE_HOOK=this;
}
public static void main(String[] args) throws Throwable{
System.out.println("test");
// TODO Auto-generated method stub
SAVE_HOOK=new FinalizeEscapeGC();
//对象第一次自救
SAVE_HOOK=null;
System.gc();
//finalize处于低优先级执行队列中
Thread.sleep(500);
if(SAVE_HOOK!=null)
SAVE_HOOK.isAlive();
else
{
System.out.println("is dead");
}
//以下代码重复执行 但是自救失败
SAVE_HOOK=null;
System.gc();
//finalize处于低优先级执行队列中
Thread.sleep(500);
if(SAVE_HOOK!=null)
SAVE_HOOK.isAlive();
else
{
System.out.println("is dead");
}
}
}
打印结果:
finalize method execute
still alive
is dead
第一次自救成功,第二次却失败了 因为对象的fianlize方法只会被执行一次 , finalize()方法是一个代价很高的函数,不确定性大,无法保证各个对象的执行调用顺序,所以任何情况下都不推荐使用finalize()方法。