深入浅出JVM(十一)之如何判断对象“已死”

简介: 深入浅出JVM(十一)之如何判断对象“已死”

在方法中会创建大量的对象,对象并不一定是全局都会使用的,并且Java虚拟机的资源是有限的

当JVM(Java虚拟机)判断对象不再使用时,就会将其回收,避免占用资源

那么JVM是如何判断对象不再使用的呢?

本篇文章将围绕判断对象是否再使用,深入浅出的解析引用计数法、可达性分析算法以及JVM如何判断对象是真正的“死亡”(不再使用)

判断对象已死

引用计数算法

引用计数算法判断对象已死

在对象添加一个引用计数器,有地方引用此对象该引用计数器+1,引用失效时该引用计数器-1;当引用计数器为0时,说明没有任何地方引用对象,对象可以被回收

image.png

但是该方法无法解决循环引用(比如对象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();
     }
 }

image.png

可达性分析算法

Java使用可达性分析算法,可以解决循环引用

可达性分析算法判断对象已死

  • GC Roots对象开始,根据引用关系向下搜索,搜索的过程叫做引用链
  • 如果通过GC Roots可以通过引用链达到某个对象则该对象称为引用可达对象
  • 如果通过GC Roots到某个对象没有任何引用链可以达到,就把此对象称为引用不可达对象,将它放入引用不可达对象集合中(如果它是首个引用不可达对象节点,那它就是引用不可达对象根节点)

image.png

可以作为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方法只会被执行一次,后续再不可达则会被直接认为对象不再使用

最后

  • 参考资料
  • 《深入理解Java虚拟机》

本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~

觉得菜菜写的不错,可以点赞、关注支持哟~

有什么问题可以在评论区交流喔~


相关文章
|
14天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
3月前
|
存储 Java 程序员
Java中对象几种类型的内存分配(JVM对象储存机制)
Java中对象几种类型的内存分配(JVM对象储存机制)
83 5
Java中对象几种类型的内存分配(JVM对象储存机制)
|
4月前
|
存储 监控 算法
(六)JVM成神路之GC基础篇:对象存活判定算法、GC算法、STW、GC种类详解
经过前面五个章节的分析后,对于JVM的大部分子系统都已阐述完毕,在本文中则开始对JVM的GC子系统进行全面阐述,GC机制也是JVM的重中之重,调优、监控、面试都逃不开的JVM话题。
127 8
|
4月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
126 8
|
5月前
|
算法 Java
Java垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一种自动内存管理机制,用于在运行时自动回收不再使用的对象所占的内存空间
【6月更文挑战第18天】Java的GC自动回收内存,包括标记清除(产生碎片)、复制(效率低)、标记整理(兼顾连续性与效率)和分代收集(区分新生代和老年代,用不同算法优化)等策略。现代JVM通常采用分代收集,以平衡性能和内存利用率。
71 3
|
5月前
|
存储 缓存 算法
JVM对象创建与内存分配机制
该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
42 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4