详解JVM如何判断一个对象是否可以被回收

 更新时间:2023年11月23日 08:17:20   作者:Shawn_Shawn  
在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收,下面我们就来看看JVM是如何判断一个对象是否可以被回收的吧

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。

首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以被回收呢?一般都是通过引用计数法,可达性算法。

引用计数法

对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效(改为引用其他对象,赋值为null,或者生命周期结束)则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象,一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。

public void f(){
    Object a = new Object(); // 对象a引用计数为1
    g(a);
    // 退出g(a),对象b的生命周期结束,对象a引用计数为1
}// 退出f(), 对象a的生命周期结束,引用计数为0

public void g(Object a){
    Object b = a; // 对象a引用计数为2
	Object c = a; // 对象a引用计数为3
    Object d = a; // 对象a引用计数为4
	d = new Object(); // 对象a引用计数为3
	c = null; // 对象a引用计数为2
}

引用计数法实现起来比较容易,但是存在一个严重的问题,那就是无法检测循环依赖。如下所示:

public class A{
    public B b;
	public A(){
    
    }
}

public class A{
    public A a;
    public B(){
    
    }
}

A a = new A(); // a的计数为1
B b = new B(); // b的计数为1
a.b = b; // b的计数为2
b.a = a; // a的计数为2
a = null; // a的计数为1
b = null; // b的计数为1

最终a,b的计数都为1,无法被识别为垃圾,所以无法被回收。

Python使用的就是引用计数算法,Python的垃圾回收机制,很大一部分是为了处理可能产生的循环引用,是对引用计数的补充。

虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。

可达性算法

介绍

Java最终并没有采用引用计数算法,JVM的主流垃圾回收器采取的是可达性分析算法。

我们把对象之间的引用关系用数据结构中的有向图来表示。图中的顶点表示对象。如果对象A中的变量引用了对象B,那么,我们便在对象A对应的顶点和对象B对应的顶点之间画一条有向边。

在有向图中,有一组特殊的顶点,叫做GC Roots。哪些对象可以作为GC Roots呢?

  • 系统加载的类:rt.jar。
  • JNI handles。
  • 线程运行栈上所有引用,包括方法参数,创建的局部变量等。
  • 已启动未停止的java线程。
  • 已加载类的静态变量。
  • 用于同步的监控,调用了对象的wait()/notify()/notifyAll()。

JVM以GC Roots为起点,遍历(深度优先遍历或广度优先遍历)整个图,可以遍历到的对象为可达对象,也叫做存活对象,遍历不到的对象为不可达对象,也叫做死亡对象。死亡对象会被虚拟机当做垃圾回收。

JVM实际上采用的是三色算法来遍历整个图的,遍历走过的路径被称为reference chain。

  • Black: 对象可达,且对象的所有引用都已经扫描了(“扫描”在可以理解成遍历过了或加入了待遍历的队列)
  • Gray: 对象可达,但对象的引用还没有扫描过(因此 Gray 对象可理解成在搜索队列里的元素)
  • White: 不可达对象或还没有扫描过的对象

引用级别

遍历到的对象一定会存活吗?事实上,JVM会根据对象A对对象B的引用强不强烈作出相应的回收措施。

基于此JVM根据引用关系的强烈,将引用关系分为四个等级:强引用,软引用,弱引用,虚幻引用。

强引用

类似Object obj = new Object() 这类的引用都属于强引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,只有在和GC Roots断绝关系时,才会被回收。

如果要对强引用进行垃圾回收,需要设置强引用对象为 null,或者让其超出对象的生命周期范围,则认为改对象不存在引用。类似obj = null;

参考代码:

public void clear() {
    modCount++;
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

软引用

用于描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。可以使用SoftReference 类来实现软引用。

Object obj = new Object();
SoftReference<Object> softRef = new SoftReference(obj);

弱引用

也是用于描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可以使用WeakReference 类来实现弱引用。

Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
obj = null;
System.gc();
TimeUnit.SECONDS.sleep(200);
System.out.println(weakReference.get());
System.out.println(weakReference.isEnqueued());

虚引用

它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置一个虚引用关联的唯一目的是能在这个对象被垃圾回收时收到一个系统通知。可以通过PhantomReference 来实现虚引用。

Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue);
System.out.println(phantomReference.get());
System.out.println(phantomReference.isEnqueued());

基于虚引用,有一个更加优雅的实现方式,那就是Java 9以后新加入的Cleaner,用来替代Object类的finalizer方法。

STW

虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。我们把运行应用程序的线程叫做用户线程,把执行垃圾回收的线程叫做垃圾回收线程,如果在执行垃圾回收线程的同时还在执行用户线程,那么对象的引用关系可能会在垃圾回收途中被用户线程修改,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)

误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致程序出错。

为了解决漏报的问题,保证垃圾回收线程不会被用户线程打扰,最简单粗暴的方式就是在垃圾回收的过程中,暂停用户线程,直到垃圾回收结束,再恢复用户线程,这就是STW(STOP THE WORLD)。

但是如果STW的时间过程,就会严重影响程序的性能,因此优化垃圾回收过程,尽量减少STW的时间,是垃圾回收器努力优化的方向,

安全点

上述除了STW的响应时间的问题,还有另外一个问题,就是如何从一个正确的状态停止,再从这个状态正确恢复。Java虚拟机中的STW是通过安全点(safepoint)机制来实现的。当Java虚拟机收到STW请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。

当然,安全点的初始目的并不是让用户线程立刻停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析,才能找到完整GC Roots。

是不是所有的用户线程在垃圾回收的时候都要停止呢?实际上,JVM也做了优化,如果某个线程处于安全区(不会改变对象引用关系的一段连续的代码区间),那么这个线程不需要停止,可以和垃圾回收线程并行执行。一旦离开安全区,JVM会检查是否处于STW阶段,如果是,则需要阻塞该线程,等垃圾回收完再恢复。

以上就是详解JVM如何判断一个对象是否可以被回收的详细内容,更多关于JVM对象回收的资料请关注脚本之家其它相关文章!

相关文章

  • java中的十个大类总结

    java中的十个大类总结

    java.lang.string字符串类将是无可争议的冠军在任何一天的普及和不可以否认。这是最后一个类,用来创建操作不可变字符串字面值
    2013-10-10
  • 解决idea配置Tomcat Deployment没有artifact选项的问题

    解决idea配置Tomcat Deployment没有artifact选项的问题

    今天在配置的时候tomcat deployment中却找不到artifact,没有artifact就不能打成war包上传到服务器了,那么怎么解决没有artifact选项的问题呢,今天通过本文给大家分享idea配置Tomcat Deployment没有artifact选项的解决方案,一起看看吧
    2023-10-10
  • Java从网络读取图片并保存至本地实例

    Java从网络读取图片并保存至本地实例

    这篇文章主要为大家详细介绍了Java从网络读取图片并保存至本地的实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • SpringBoot超详细讲解yaml配置文件

    SpringBoot超详细讲解yaml配置文件

    这篇文章主要介绍了SpringBoot中的yaml配置文件问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • SpringBoot3.0+SpringSecurity6.0+JWT的实现

    SpringBoot3.0+SpringSecurity6.0+JWT的实现

    本文主要介绍了SpringBoot3.0+SpringSecurity6.0+JWT的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-11-11
  • IDEA2020.2创建springboot项目卡死在reading maven project的问题

    IDEA2020.2创建springboot项目卡死在reading maven project的问题

    这篇文章主要介绍了关于2020.2IDEA用spring Initializr创建maven的springboot项目卡死在reading maven project的问题描述及解决方法,感兴趣的朋友跟随小编一起看看吧
    2020-09-09
  • MyBatis动态sql查询及多参数查询方式

    MyBatis动态sql查询及多参数查询方式

    这篇文章主要介绍了MyBatis动态sql查询及多参数查询方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • Java ClassLoader虚拟类实现代码热替换的示例代码

    Java ClassLoader虚拟类实现代码热替换的示例代码

    本文主要介绍了Java ClassLoader虚拟类实现代码热替换的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • 一文带你快速了解java中的static关键词

    一文带你快速了解java中的static关键词

    这篇文章主要给大家介绍了关于java中static关键词的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • feign post参数对象不加@RequestBody的使用说明

    feign post参数对象不加@RequestBody的使用说明

    这篇文章主要介绍了feign post参数对象不加@RequestBody的使用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10

最新评论