JVM深入理解(6)——引用

写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。

介绍

JDK1.2后,java对引用的概念进行了扩充,分为强引用、软引用、弱引用、虚引用。

强引用(StrongReference)

最传统的“引用”,指在程序代码中普遍存在的引用赋值,无论任何情况下,只要强引用存在,垃圾回收器永远不会回收掉被引用的对象

默认情况的引用类型都是强引用。

强引用的对象都是可触及的(详见finalize机制),垃圾回收永远不会回收。

当且仅当一个对象没有其它引用关系,且当前引用被赋值为null(或过了作用域)就可以被垃圾回收了。

所以,强引用是造成java内存泄漏的主要原因之一

特点:

  1. 强引用可以直接访问目标对象
  2. 所指向对象在任何时候都不会被系统回收,而宁愿抛出OOM异常
  3. 强引用可能导致内存泄漏

软引用(SoftReference)- 内存不足即回收

在发生内存溢出之前,会把对象列入回收范围内进行第二次回收。如果回收后还没有足够内存,才会抛出内存溢出异常。

即先回收不可触及的对象,如果还没有足够内存,则回收软引用关联对象。

通常用来实现内存敏感的缓存。

垃圾回收期在某个时刻决定回收可达对象的时候,会清理掉软引用,并可选地把引用存放在一个引用队列。

内存够:不会回收

代码示例

设置VM参数-Xms10m -Xmx10m -XX:+PrintGCDetails

    public static void main(String[] args) {
        byte[] b = new byte[1024*1024*2];

        SoftReference<byte[]> s = new SoftReference<>(new byte[1024*1024*3]);
        System.out.println(s.get());
        try{
            b=new byte[1024*1024*4];
        }finally {
            System.out.println(s.hashCode());
        }
        System.out.println(s.get());

    }

我们发现,在内存不够的情况下,第一个打印语句不为null,第二个打印语句为null,没有抛出OOM异常,说明已经被回收。

怎么证明当内存足够时,垃圾回收器不会回收软引用呢?

    public static void main(String[] args) {
        byte[] b = new byte[1024*1024*2];

        SoftReference<byte[]> s = new SoftReference<>(new byte[1024*1024*3]);

        System.gc();
        System.out.println(s.get());
    }

当我们强制使用System.gc()时,通过打印语句可以看出JVM进行了垃圾回收,然而并没有回收软引用指向的对象。

弱引用(WeakReference)- 发现即回收

只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论空间是否足够,都会回收掉只被弱引用关联的对象。

但是,由于垃圾回收器的线程优先级很低,并不一定能很快发现持有弱引用的对象,这种情况下,弱引用对象可存在较长时间。

弱引用和软引用一样,在构造弱引用时,也可以制定一个引用队列,当弱引用对象被回收时,就会加入指定队列,通过队列可以跟踪对象回收情况。

虚引用(PhantomReference)

是否有虚引用,完全不会对对象的生存时间构成影响,也无法通过虚引用获取一个对象的实例,唯一作用是在被回收时,收到一个系统通知。

当试图通过虚引用的get()方法得到对象时,只会得当null

注意,必须要提供引用队列作为参数,当垃圾回收器准备回收对象时,如果还有虚引用,就会在回收对象后,将虚引用加入引用队列,以通知回收情况。

由于虚引用可以跟踪对象的回收时间,可以将资源释放操作放置在虚引用执行和记录

终结器引用(FinalReference)

这一块宋老师讲的比较少

FinalReference类仅对同一个包的类可见,意味着我们不能创建其实例。
Finalizer类继承了这个FR类

我们可以通过在类中实现finalize()方法来达到使用此引用的目的。
只要实现了finalize()方法,且方法体不为空,则就会被标记为一个finalizer(在类被加载过程中就已经被标记了)

之前讲的finalize的机制还要更复杂一些
简单来说就是:当一个finalier仅在Finalizer类的对象中被注册(即仅被这个对象引用),说明可以执行finalize()方法了(即可以被回收),所以就会把Finalizer对象放在Finalizer类的ReferenceQueue里,gc完成之前,jvm会唤醒RQ(如果RQ不为空,且注意这个唤醒并非一定能使得此线程马上获得锁),将会从队列中移出Finalizer对象,然后触发runFinalizer()方法(即得到其注册的finalizer,并会在本地方法中调用finalize()方法)。

在Finalizer类中创建了一个Finalizer线程,在run()方法里面会执行这个Finalizer类对象的runFinalizer()方法,由于优先级不是特别高,所以会有之前所说的finalize()的执行时间不确定的情况。

finalize()方法为什么只会执行一次?
当执行完第一次以后,finalizer已经和Finalizer对象脱离关系了,下次GC时就会发现没有引用再指向该finalizer,所以就不会调用其finalize()方法了。

以上都为对JVM源码分析之FinalReference完全解读的解读,若想更深入理解,可以移步这里。

小结:
一个finalizer至少得经历两次GC才能被成功清除。

  1. 第一次GC时,发现有Finalize对象里的变量强引用指向这个finalizer,故只能将其将入RQ,而不回收它。注意,GC时要获取一致性快照,即不能发生对象引用关系的变化,所以第一次GC不可能回收掉finalizer。
  2. 如果Finalizer线程抢到了运行时间,且RQ不为空,那么它可能幸运地将RQ中的Finalizer对象弹出,并调度finalizer的finalize()方法。
  3. 如果此方法没有使得这个对象复活,那么下一次GC时,对象就会被回收。所以最少是两次GC。

如果Finalizer线程还没有执行,或者没有执行到finalize()方法,那么可能会等待多个GC才能被回收。那么对象可能就会进入老年代,而老年代需要major GC或者full GC来进行回收了,这样代价就会很大了。

注意,finalize()方法被调用且对象没被复活,并不代表对象就被回收了,而是在不久的将来被回收(下一次GC)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值