关于后端:深入浅出JVM十一之如何判断对象已死

88次阅读

共计 3980 个字符,预计需要花费 10 分钟才能阅读完成。

在办法中会创立大量的对象,对象并不一定是全局都会应用的,并且 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 对象的对象

  1. 在栈帧中局部变量表中援用的对象 参数、长期变量、局部变量
  2. 本地办法援用的对象
  3. 办法区的类变量援用的对象
  4. 办法区的常量援用的对象(字符串常量池中的援用)
  5. sychronized 同步锁持有的对象
  6. JVM 外部援用(根底数据类型对应的 Class 对象、零碎类加载器、常驻异样对象等)
  7. 跨代援用
  • 毛病:

    • 应用可达性剖析算法必须在放弃一致性的快照中进行(某时刻静止状态)
    • 这样在进行 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 公布!

正文完
 0