JVM是如何判定对象为垃圾的(可达性分析算法)

简介: 被判定为垃圾的对象或者内存区域会被垃圾收集器回收。那么什么样的对象或者内存区域会被判定为垃圾呢?下面就要说起经常作为垃圾判定依据的可达性分析算法与引用计数法了。这两种算法,都是经常被用作垃圾判定的算法,下面说下这两种算法。

前言:被判定为垃圾的对象或者内存区域会被垃圾收集器回收。那么什么样的对象或者内存区域会被判定为垃圾呢?下面就要说起经常作为垃圾判定依据的可达性分析算法引用计数法了。这两种算法,都是经常被用作垃圾判定的算法,下面说下这两种算法。


一.引用计数法


为对象添加一个引用计数器,当有一个对象引用了该对象时,引用计数器就加一,当引用失效时引用计数器就减一,当引用计数器的值为零时,就说明该对象变成了垃圾。


二.可达性分析算法


从一系列被称为“GC Roots”的节点集根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到GC Roots之间没有任何引用链,或者用图论的方式来说GC Roots到这个对象不可达时,则证明该对象是不可达的,但此时并不能判定该对象是垃圾,只能说该对象不可达。判定一个对象为垃圾,要经过以下步骤:


20210220170210751.png


如上图①:第一步使用可达性分析算法判断GC Roots到对象之间是否还有引用链,有则存活,没有引用链则对对象进行第一次标记。


如上图②:第二步判断被标记的对象是否有重写finalize()方法,若该方法已经重写过且被执行过或者未重写该方法则对象判断为死亡,否则将对象放入F-Queue队列中,稍后JVM会启动一个低调度优先级的Finalize线程去执行F-Queue队列中的各个对象的finalize()方法。


如上图③:第三步在执行各个对象的finalize()方法时,是各个对象完成自我救赎,不被判定为垃圾的最后机会,如果在finalize()方法中当前对象成功与GC Roots建立了引用链。则会判定为存活,否则会被进行第二次标记,被两次标记的对象就是被判定为死亡的对象,在下次垃圾回收时,会被垃圾收集器回收掉。


三.看到这里肯定大部分都是这个疑问?GC Roots是什么?凭什么用他判定对象是否可达?

1.GC Roots是什么?


GC Roots就是一组可以作为访问根节点的一组对象集,常见的GC Roots有以下这些
①在虚拟机栈(局部变量表)中引用的对象。

②方法区中静态属性引用的对象(JDK8之后静态变量在堆中)。

③方法区中常量引用的对象(这里指运行时常量池中的常量)。

④本地方法中JNI(Native方法)引用的变量。

⑤JVM内部的引用,如基本数据类型对应的Class对象等。


2.凭什么用他们判定对象是否可达?


分析前四种可以作为GC Roots的对象可以发现,他们都有一个特征,这些对象的引用都存在堆内存以外,那为什么不是只存在堆中的对象作为GC Roots呢?因为只存在堆中的对象,比如实例变量,他的最终调用者肯定还是前四种对象(这里可以仔细思考下之前写过的代码)。那这四种常见的可以作为GC Roots的对象,其实就是最常见的引用调用的最外层结构。所以他们作为根节点,向下搜索引用关系是合理的。


四. 我们使用的虚拟机使用哪种算法判定垃圾?


我们最常用的虚拟机HotSpot一直使用的都是可达性分析算法,那为什么HotSpot不使用引用计数法呢?分析下这两种算法的优缺点。


引用计数法


优点是判定效率高,缺点是占用一定内存,最重要的缺点就是无法解决相互引用的问题,当对象相互引用时,引用计数器的值最少也是1.如下方代码所示

objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;


当我们使用objA=null;objB=null;以后分别各有1个引用指向objA,objB。采用引用计数法这两个引用是无法消除的,因为 objA.instance失效需要先让objA失效,objA失效需先让objB.instance失效(因为objB.instance指向objA),objB.instance想要失效需要objB失效,objB失效需要先让objA.instance失效(因为objA.instance指向objB),所以这样就陷入了死循环,导致objA、objB的引用计数器一直都是1,便无法被虚拟机回收,这也就是引用计数法的最大弊端。 如下图:


20210224100343239.png



1.可达性分析算法:
优点是不必为每个对象都分配一小块内存用以存储引用计数,能轻松解决相互引用等其他场景。缺点也是相对于引用计数来说的,判定效率不如引用计数法


五.说引用必须说下java中的强引用、软引用、弱引用、虚引用(从左到右引用关系由强到弱)。


强、软、弱、虚出现的背景:如果栈中的reference中存储的是代表另一块内存或对象的起始地址,我们称reference代表某个内存或某个对象的引用,这种对对象的定义并没有什么不对,只是随着JDK版本的不断更新,这种定义已经有些狭义了,比如对于那些比较鸡肋的对象只使用这种方式定义就显得不足,因此引入了强引用、软引用、弱引用、虚引用等用来描述不同场景下的对象引用。


1.强引用:强引用指的是最传统的引用定义,指的是我们在写java代码时最常用的一种引用赋值,如Object obj = new Object();这也是强、软、弱、虚定义之前定义引用的方式。


何时回收强引用?


强引用的引用本身就是可以作为GC Root的存在,所以只要引用关系存在,那么强引用就一直不会被回收,所以在不使用时置null即可。


2.软引用:软引用用来描述那些还有用,但是非必须的对象,使用SoftReference来修饰对象,使用get方法可以获得被弱软引用引用的对象,定义时如下:


Object obj = new Object();
String str = "abc";
SoftReference<String> sf = new SoftReference<String>(str);
String strRefe =  sf.get();
System.out.println(strRefe);
输出结果:
abc
Process finished with exit code 0


何时回收软引用?


jvm会在内存吃紧时,会尝试去清理软引用,但是不一定会能回收的了软引用的对象,jvm只是在即将OOM时会去尝试清理堆中的软引用对象,如果清除不了还是会报OOM的。如果有人看到过这种说法“jvm会在内存溢出前,清空软引用引用的对象”,请知晓,这是错误的,下面验证下这种错误的说法(因为很多人这么说),代码如下:


import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class TestHeapOom {
  static class OOMObject{
      String name;
      public OOMObject(String name){
          this.name = name;
      }
  }
  // 虚拟机配置:-Xmx10M -Xms10M
  public static void main(String[] args){
      int n =0;
      List<OOMObject> list = new ArrayList<OOMObject>();
      SoftReference<List<OOMObject>> sr = new SoftReference<List<OOMObject>>(list);
      System.out.println(sr.get().size());
      while (true){
            list.add(new OOMObject("a"+n));
          System.out.println(sr.get().size());
      }
  }
}


上方程序是用软引用关联一个list,然后建立一个循环不断的往list中加入对象,这种情况10M的堆内存很快会用光,正常情况下内存溢出,会报OOM:Java heap space,这里输出如下展示:


106280
106281
106282
106283
106284
106285
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
   at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)
   at java.lang.StringBuilder.<init>(StringBuilder.java:89)
   at sgcc.supplier.pojo.model.queues.TestHeapOom.main(TestHeapOom.java:23)
Process finished with exit code 1


很显然这里并没有抛出OOM:Java heap space,而是抛出的OOM:GC overhead limit exceeded(不是必现),为什么抛出的是这个呢?首先解释下这个错误的意思,该错误表示cpu98%的时间都在做内存回收,但是回收到的内存依然很小,不足以支撑系统使用,系统即将崩溃。就会报这种问题,介绍软引用时已经说明了,软引用会在内存溢出之前被尝试回收,因为在上面的例子中内存即将溢出,jvm便在一直尝试回收软引用list,但是list仍是可达状态,回收不了,但系统又一直尝试软引用,便出现了这个错误:OOM:GC overhead limit exceeded。所以说“jvm会在内存溢出前,清空软引用引用的对象”这种说法是错误的,回不回收软引用对象最终依据还是可达性分析算法,jvm只是会在内存紧张时尝试回收软引用的对象。,换一种说法“jvm会在内存溢出前,清空只被软引用引用的对象”这种说法就是正确的。验证代码如下:


import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class TestHeapOom {
  static class OOMObject{
      String name;
      public OOMObject(String name){
          this.name = name;
      }
  }
  // 虚拟机配置:-Xmx10M -Xms10M -XX:+UseSerialGC
  public static void main(String[] args){
      int n =0;
      //List<OOMObject> list = new ArrayList<OOMObject>();
      SoftReference<List<OOMObject>> sr = new SoftReference<List<OOMObject>>(new ArrayList<OOMObject>());
      while (true){
          sr.get().add(new OOMObject("a"+n));
          System.out.println(sr.get().size());
      }
  }
}

从上方的程序中,可以轻易的看出新创建出的ArrayList对象只被软引用关联了,除了软引用没有其他的引用关联,运行结束产生如下日志,报出的是空指针,所以在垃圾回收时,这个只被软引用关联的对象是被清空了的。另外从另一个角度也可以解释为什么只被软引用关联的对象会被清空:判定对象是否是垃圾的依据是可达性分析算法,可达性分析算法中的要求GC Roots到对象之间有引用链才说明对象可达,而软引用是不能作为GC Roots的。


132782
132783
132784
132785
132786
132787
132788
132789
132790
132791
132792
132793
Exception in thread "main" java.lang.NullPointerException
  at sgcc.supplier.pojo.model.queues.TestHeapOom.main(TestHeapOom.java:22)
Process finished with exit code 1


3.弱引用:弱引用被用来描述那些非必须的对象,java中使用WeakReference来描述弱引用,使用get方法可以获得被弱引用引用的对象。弱引用的使用方法如下:


String str = "abc";
WeakReference<String> wf = new WeakReference<String>(str);
String strRew = wf.get();
System.out.println(strRew);


何时回收弱引用?


弱引用对象在下次垃圾回收时系统会尝试进行回收,如果对象不可达就会被回收掉,对象如果是可达状态依然不会回收,验证代码如下:


import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class TestHeapOomWeak {
  static class OOMObject{
      String name;
      public OOMObject(String name){
          this.name = name;
      }
  }
  // 虚拟机配置:-Xmx10M -Xms10M
  public static void main(String[] args){
      int n =0;
      List<OOMObject> list = new ArrayList<OOMObject>();
      WeakReference<List<OOMObject>> wr = new WeakReference<List<OOMObject>>(list);
      System.out.println(wr.get().size());
      while (true){
          list.add(new OOMObject("a"+n));
          System.out.println(wr.get().size());
      }
  }
}


输出结果如下,可见系统一直在进行垃圾回收,即将崩溃,但是引用却没有失效,所以弱引用并不在垃圾回收时一定被回收(这里这么解释是因为很多人说会在下一次垃圾回收时回收掉)回不回收弱引用对象最终依据还是可达性分析算法,jvm只是会在下一次GC时尝试回收弱引用


106273
106274
106275
106276
106277
106278
106279
106280
106281
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
   at java.lang.Integer.toString(Integer.java:401)
   at java.lang.String.valueOf(String.java:3099)
   at java.io.PrintStream.print(PrintStream.java:597)
   at java.io.PrintStream.println(PrintStream.java:736)
   at sgcc.supplier.pojo.model.queues.TestHeapOomWeak.main(TestHeapOomWeak.java:25)
Process finished with exit code 1


但是如果某个对象只被弱引用引用了,那下一次垃圾回收就会把该对象回收掉。验证代码如下:


import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class TestHeapOomWeak {
  static class OOMObject{
      String name;
      public OOMObject(String name){
          this.name = name;
      }
  }
  public static void main(String[] args){
      int n =0;
      //List<OOMObject> list = new ArrayList<OOMObject>();
      WeakReference<List<OOMObject>> wr = new WeakReference<List<OOMObject>>(new ArrayList<OOMObject>());
      System.out.println(wr.get().size());
      while (true){
          wr.get().add(new OOMObject("a"+n));
          System.out.println(wr.get().size());
      }
  }
}


如下方所示,输出变成了空指针,而不是OOM,因为在GC后只被弱引用关联的对象被回收了,后面通过弱引用get的对象就成了null,便报了空指针。


28532
28533
28534
28535
Exception in thread "main" java.lang.NullPointerException
   at sgcc.supplier.pojo.model.queues.TestHeapOomWeak.main(TestHeapOomWeak.java:24)
Process finished with exit code 1


4.虚引用:又被称为灵幻引用,使用PhantomReference定义虚引用,使用get方法得到的永远是null,同时虚引用定义时必须传入一个ReferenceQueue 队列,对象被清理时,该对象的引用会被放入该队列中,被虚引用关联的对象无法通过get方法获得该对象的实例(软引用,弱引用都可以)所以只被虚引用关联的对象其实立马就会被回收掉,虚引用的声明方式如下


String str = "abc";
ReferenceQueue rq = new ReferenceQueue();
PhantomReference<String> pf = new PhantomReference<String>(str,rq);
System.out.println(pf.get());


相关文章
|
9月前
|
监控 Java Unix
6个Java 工具,轻松分析定位 JVM 问题 !
本文介绍了如何使用 JDK 自带工具查看和分析 JVM 的运行情况。通过编写一段测试代码(启动 10 个死循环线程,分配大量内存),结合常用工具如 `jps`、`jinfo`、`jstat`、`jstack`、`jvisualvm` 和 `jcmd` 等,详细展示了 JVM 参数配置、内存使用、线程状态及 GC 情况的监控方法。同时指出了一些常见问题,例如参数设置错误导致的内存异常,并通过实例说明了如何排查和解决。最后附上了官方文档链接,方便进一步学习。
1373 4
|
8月前
|
Arthas 监控 Java
Arthas vmtool(从 jvm 里查询对象,执行 forceGc)
Arthas vmtool(从 jvm 里查询对象,执行 forceGc)
508 16
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
900 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
287 6
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
垃圾识别分类系统。本系统采用Python作为主要编程语言,通过收集了5种常见的垃圾数据集('塑料', '玻璃', '纸张', '纸板', '金属'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对图像数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。然后使用Django搭建Web网页端可视化操作界面,实现用户在网页端上传一张垃圾图片识别其名称。
557 0
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
|
缓存 Java
JVM对象引用
本次课程聚焦JVM对象引用,涵盖强引用、软引用、弱引用和虚引用。强引用是最常见的引用类型,确保对象不会被垃圾回收器回收,适用于需要确保对象存活的场景;软引用在内存不足时会被优先回收,常用于缓存;弱引用的对象随时可能被回收,适合临时对象;虚引用最弱,主要用于接收对象回收通知,进行资源清理。通过合理选择引用类型,可优化内存管理,避免内存泄露。
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
240 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
3月前
|
机器学习/深度学习 算法 机器人
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
391 0
|
3月前
|
数据采集 分布式计算 并行计算
mRMR算法实现特征选择-MATLAB
mRMR算法实现特征选择-MATLAB
265 2
|
4月前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
269 3