垃圾回收—对象是否已死
-
判断对象是否存活—引用计数算法❶
实现原理:给每个对象添加一个引用计数器,每当此对象被某个地方引用时,计数值+1;引用失效时,计数值-1。当计数值为0时,表示对象已经不能被使用。
-
引用计数算法的特点
-
实现简单、效率高,但是无法检测出循环引用❷。
-
-
判断对象是否存活-可达性分析算法❸
-
可达性分析算法
-
在主流的商用程序语言如Java、C#等的主流实现中,都是通过可达性分析(Reachability Analysis)来判断对象是否存活的。此算法的基本思路就是通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
-
栈帧中的局部变量表中的reference引用所引用的对象,譬如各个线程被调用的方法堆栈使用到的参数、局部变量、临时变量等
-
方法区中类静态属性引用的对象。譬如Java类的引用类型静态变量。
-
方法区中final常量引用的对象
-
本地方法栈中JNI(Native方法)引用的对象
-
Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如 NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
-
所有被同步锁(synchronized关键字) 持有的对象。
-
反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等。
-
判断对象是否存活
即使在可达性分析算法中判定为不可达的对象, 也不是“非死不可”的, 这时候它们暂时还处于“缓 刑”阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程:
第一次标记:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记, 随后进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。
没有必要:
假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必要执行”。
有必要:
如果这个对象被判定为确有必要执行finalize()方法, 那么该对象将会被放置在一个名为F-Queue的 队列之中, 并在稍后由一条由虚拟机自动建立的、 低调度优先级的Finalizer线程去执行它们的finalize() 方法。finalize()方法是对 象逃脱死亡命运的最后次机会, 稍后收集器将对F-Queue中的对象进行第二次小规模的标记, 如果对 象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可, 譬如把自己 (this关键字) 赋值给某个类变量或者对象的成员变量, 那在第二次标记时它将被移出“即将回收”的集 合;如果对象这时候还没有逃脱, 那基本上它就真的要被回收了。
注意:
Finalizer线程去执行它们的finalize() 方法, 这里所说的“执行”是指虚拟机会触发这个方法开始运行, 但并不承诺一定会等待它运行结束。这样做的原因是, 如果某个对象的finalize()方法执行缓慢, 或者更极端地发生了死循环, 将很可能导 致F-Queue队列中的其他对象永久处于等待, 甚至导致整个内存回收子系统的崩溃。
附:
❶
-
引用计数算法常用来说明垃圾回收算法的机制,但是很少为各种语言所有,其中COM采用的就是引用计数算法(未验证)。
❷
-
循环引用
-
当对象A和对象B相互引用了对方作为自己的成员变量,只有当自己销毁时,才能将自己成员变量所指向的对象的引用计数减1。即:对象A的销毁依赖对象B的销毁,反之亦然。这样子就造成了循环引用吗。
-
-
解决循环引用的方法
-
明确知道这里会存在循环引用,在合适的位置主动断开环中的一个引用,使对象得到回收。
-
使用弱引用,
-
/**
## 引用计数算法之GC
譬如有A和B两个对象,他们都互相引用,除此之外都没有任何对外的引用,那么理论上A和B都可以被作为垃圾回收掉,但实际如果采用引用计数算法,则A、B的引用计数都是1,并不满足被回收的条件,如果A和B之间的引用一直存在,那么就永远无法被回收了。
#由于Hotspot使用的是可达性分析算法,所以此示例循环引用的垃圾对象也可被回收
*/
//循环引用示例
public class GcDemo{
public static void main(String[] args) {
GcObject obj1 = new GcObject(); //Step1
GcObject obj2 = new GcObject();//Step2
obj1.instance = obj2; //Step3
obj2.instance = obj1;// //Step4
obj1 = null; //Step5
obj2 = null; //Step6
}
}
public class GcObject{
public GcObject instance = null;
}
/**
采用引用计数算法时:
第一步:GcObject实例1被obj1引用,所以它的引用数+1,为1
第二步:GcObject实例2被obj2引用,所以它的引用数+1,为1
第三步:obj1的instance属性指向obj2,而obj2指向GcObject实例2,故GcObject实例2引用+1,为2
第四步:obj2的instance属性指向obj1,而obj1指向GcOjbect实例1,故GcObject实例1引用+1,为2
到此,发现GcObject实例1和实例2的计数引用都不为0,那么如果采用的引用计数算法的话,那么这两个实例所占的内存将得不到释放,这便产生了内存泄露。
PS:注意想一下,为什么是obj的instance属性,而不是写成obj本身?如果写成obj, 如下:
第五步:obj1不再指向GcOjbect实例1,则GcOjbect实例1的引用计数减1,结果为1.
第六步:obj2不再指向GcOjbect实例2,则GcOjbect实例2引用计数减1,结果为1.
*/
❸:
/**
* @Description : 可达性分析方法的对象自我拯救演示
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次, 因为一个对象的finalize()方法最多只会被系统自动调用一次
* @Author : Future Buddha
* @Date: 2021-07-25 16:10
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
isDead();
}
//下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
isDead();
}
}
private void isAlive() {
System.out.println("yes, i am still alive :)");
}
private static void isDead() {
System.out.println("no, i am dead :(");
}
}