在办法中会创立大量的对象,对象并不一定是全局都会应用的,并且 Java 虚拟机的资源是无限的
当 JVM(Java 虚拟机)判断对象不再应用时,就会将其回收,防止占用资源
那么 JVM 是如何判断对象不再应用的呢?
本篇文章将围绕判断对象是否再应用,深入浅出的解析援用计数法、可达性剖析算法以及 JVM 如何判断对象是真正的“死亡”(不再应用)
判断对象已死
援用计数算法
援用计数算法判断对象已死
在对象增加一个援用计数器,有中央援用此对象该援用计数器 +1,援用生效时该援用计数器 -1;当援用计数器为 0 时,阐明没有任何中央援用对象,对象能够被回收
然而该办法无奈解决 循环援用(比方对象 A 的字段援用了对象 B,对象 B 的字段援用了字段 A,此时都将 null 赋值给对象 A,B 它们的援用计数器上都不为 0,也就是示意对象还在被援用,但实际上曾经没有援用了)
- 长处 : 标记“垃圾”对象简略,高效
- 毛病: 无奈解决循环援用,存储援用计数器的空间开销,更新援用记数的工夫开销
因为 无奈解决循环援用所以 JVM 不应用援用计数法
援用计数办法罕用在不存在循环援用的时候,比方 Redis 中应用援用计数,不存在循环援用
证实 Java 未采纳援用计数算法
public class ReferenceCountTest {
// 占用内存
private static final byte[] MEMORY = new byte[1024 * 1024 * 2];
private ReferenceCountTest reference;
public static void main(String[] args) {ReferenceCountTest a = new ReferenceCountTest();
ReferenceCountTest b = new ReferenceCountTest();
// 循环援用
a.reference = b;
b.reference = a;
a = null;
b = null;
// System.gc();}
}
可达性剖析算法
Java 应用可达性剖析算法,能够解决循环援用
可达性剖析算法判断对象已死
-
从
GC Roots
对象开始,依据援用关系向下搜寻,搜寻的过程叫做 援用链- 如果通过
GC Roots
能够通过援用链达到某个对象则该对象称为 援用可达对象 - 如果通过
GC Roots
到某个对象没有任何援用链能够达到,就把此对象称为 援用不可达对象 ,将它放入 援用不可达对象汇合 中(如果它是首个援用不可达对象节点,那它就是援用不可达对象根节点)
- 如果通过
能够作为 GC Roots 对象的对象
- 在栈帧中局部变量表中援用的对象 参数、长期变量、局部变量
- 本地办法援用的对象
- 办法区的类变量援用的对象
- 办法区的常量援用的对象(字符串常量池中的援用)
- 被
sychronized
同步锁持有的对象 - JVM 外部援用(根底数据类型对应的 Class 对象、零碎类加载器、常驻异样对象等)
- 跨代援用
-
毛病:
- 应用可达性剖析算法必须在放弃一致性的快照中进行(某时刻静止状态)
- 这样在进行 GC 时会导致 STW(Stop the Word)从而让用户线程短暂进展
真正的死亡
真正的死亡起码要通过 2 次标记
- 通过 GC Roots 通过可达性剖析算法,失去某对象不可达时,进行第一次标记该对象
-
接着进行一次筛选(筛选条件: 此对象是否有必要执行
finalize()
)- 如果此对象没有重写
finalize()
或 JVM 曾经执行过此对象的finalize()
都将被认为此对象没有必要执行finalize()
,这个对象真正的死亡了 -
如果认为此对象有必要执行
finalize()
则会把该对象放入F-Queue
队列中,JVM 主动生成一条低优先级的 Finalizer 线程- Finalizer 线程是守护线程,不须要等到该线程执行完才完结程序,也就是说不肯定会执行该对象的 finalize()办法
- 设计成守护线程也是为了避免执行 finalize()时会产生阻塞,导致程序工夫很长,期待很久
- Finalize 线程会扫描
F-Queue
队列,如果此对象的finalize()
办法中让此对象从新与援用链上任一对象搭上关系,那该对象就实现自救 finalize() 办法是对象自救的最初机会
- 如果此对象没有重写
测试不重写 finalize()办法, 对象是否会自救
/**
* @author Tc.l
* @Date 2020/11/20
* @Description:
* 测试不重写 finalize 办法是否会自救
*/
public class DeadTest01 {
public static DeadTest01 VALUE = null;
public static void isAlive(){if(VALUE!=null){System.out.println("Alive in now!");
}else{System.out.println("Dead in now!");
}
}
public static void main(String[] args) {VALUE = new DeadTest01();
VALUE=null;
System.gc();
try {
// 等 Finalizer 线程执行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
isAlive();}
}
/*
Dead in now!
*/
对象并没有产生自救, 对象不再应用“已死”
测试重写 finalize()办法, 对象是否会自救
/**
* @author Tc.l
* @Date 2020/11/20
* @Description:
* 测试重写 finalize 办法是否会自救
*/
public class DeadTest02 {
public static DeadTest02 VALUE = null;
public static void isAlive(){if(VALUE!=null){System.out.println("Alive in now!");
}else{System.out.println("Dead in now!");
}
}
@Override
protected void finalize() throws Throwable {super.finalize();
System.out.println("搭上援用链的任一对象进行自救");
VALUE=this;
}
public static void main(String[] args) {VALUE = new DeadTest02();
System.out.println("开始第一次自救");
VALUE=null;
System.gc();
try {
// 等 Finalizer 线程执行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
isAlive();
System.out.println("开始第二次自救");
VALUE=null;
System.gc();
try {
// 等 Finalizer 线程执行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
isAlive();}
}
/*
开始第一次自救
搭上援用链的任一对象进行自救
Alive in now!
开始第二次自救
Dead in now!
*/
第一次自救胜利,第二次自救失败,阐明了 finalize()执行过,JVM 会认为它是没必要执行的了
重写 finalize()代价高,不能确定各个对象执行程序,不举荐应用
总结
本篇文章围绕如何判断对象不再应用,深入浅出的解析援用计数法、可达性剖析算法以及 JVM 中如何真正确定对象不再应用的
援用计数法应用计数器来记录对象被援用的次数,当产生循环援用时无奈判断对象是否不再应用,因而 JVM 没有应用援用计数法
可达性剖析算法应用从根节点开始遍历根节点的援用链,如果某个对象在援用链上阐明这个对象被援用是可达的,不可达对象则额定记录
可达性剖析算法须要在放弃一致性的快照中进行,在 GC 时会产生 STW 短暂的进展用户线程
可达性剖析算法中的根节点个别是局部变量表中援用的对象、办法中援用的对象、办法区动态变量援用的对象、办法区常量援用的对象、锁对象、JVM 外部援用对象等等
当对象不可达时,会被放在队列中由 finalize 守护线程来顺次执行队列中对象的 finalize 办法,如果第一次在 finalize 办法中搭上援用链则又会变成可达对象,留神 finalize 办法只会被执行一次,后续再不可达则会被间接认为对象不再应用
最初(一键三连求求拉~)
本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔 \~
有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下 \~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜
本文由博客一文多发平台 OpenWrite 公布!