在方法中会创建大量的对象,对象并不一定是全局都会使用的,并且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方法只会被执行一次,后续再不可达则会被直接认为对象不再使用
最后
- 参考资料
- 《深入理解Java虚拟机》
本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~
觉得菜菜写的不错,可以点赞、关注支持哟~
有什么问题可以在评论区交流喔~