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());


相关文章
|
7天前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
垃圾识别分类系统。本系统采用Python作为主要编程语言,通过收集了5种常见的垃圾数据集('塑料', '玻璃', '纸张', '纸板', '金属'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对图像数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。然后使用Django搭建Web网页端可视化操作界面,实现用户在网页端上传一张垃圾图片识别其名称。
31 0
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
存储 算法 Java
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
49 2
|
1月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
60 0
|
3月前
|
算法 Java 应用服务中间件
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
|
27天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
12天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
13天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。