Java中的四种对象引用详解和案例演示

1 对象与引用

  每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
  甚至连”Reference”都是一个类,该类代表对象的引用,它的子类有softReference、WeakReference、PhantomReference。get方法用来获取与软引用关联的对象的引用,如果该对象被回收了,则返回null。注意PhantomReference的get永远返回null。
  起源:
  在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
  JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
  所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)4 种,这 4 种引用的强度依次减弱,后三种引用都在java.lang.ref包中。
  实际上在java.lang.ref包中还有一个FinalReference(继承了Reference)、Finalizer(继承了FinalReference)和ReferenceQueue(引用队列),实际上它们都与垃圾回收有关,可以看Java中的Finalizer类以及GC二次标记过程中的Java源码解析

2 四种引用

2.1 强引用

  强引用就是指在程序代码之中普遍存在的,指创建一个对象并把这个对象赋给一个引用变量,并没有像其它三种引用一样有一个就具体的类来描述。对于强引用对象,即使内存不足,JVM宁愿抛出OutOfMemoryError (OOM)错误也不会回收这种对象。
例如:

Object object =new Object();
String str ="hello";

  如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如各种集合的移除方法。

2.2 软引用

SoftReference< People > aSoftRef=new SoftReference< People >(obj)

  软引用是用来描述一些还有用但并非必需的对象。只使用SoftReference类修饰的对象就是一个软引用对象(软可到达对象),如果一个对象只具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
  常常用于缓存操作,把某个对象标记为软引用时,当内存足够就不会回收,内存不足就会回收,比如网页缓存,图片缓存等。并且软引用都会保证在虚拟机抛出OutOfMemoryError之前已经被清除。

2.3 弱引用

WeakReference< People > reference=new WeakReference< People >(obj);

  弱引用也是用来描述非必需对象的。只使用WeakReference类修饰的对象就是一个弱引用对象(弱可达对象)。弱引用的对象相比软引用拥有更短暂的生命周期。无论内存是否足够,一旦下次垃圾回收器运行后扫描到弱引用,便会回收。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  Java中有WeakHashMap类,以及THreadLocal中的ThreadLocalMap内部节点Entry,他们都是WeakReference的实现。

2.4 虚引用

PhantomReference< People > pr = new PhantomReference< People > (object, ReferenceQueue);

  虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。
  先要纠正一种观点,一个对象被关联成虚引用,是完全有可能影响其生命周期的,虚引用有潜在的内存泄露风险,因为JVM不会自动帮助我们释放,我们必须要手动·保证它指向的堆对象是不可达的(后面测试案例会讲)。
  其实虚引用其实是持有对象引用的,但是无法通过get获取该引用, PhantomReference的get方法永远返回null。
  一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,但是跟踪对象之后,必须调用clear清除。

3 引用测试案例

代码如下:

public class ReferenceTest {
    /**
     * 1Mb内存
     */
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws InterruptedException {
        //测试强引用
//        testStrongReference();
        //测试软引用
//        testSoftReference();
        //测试弱引用
//        testWeakReference();
        //测试虚引用
//        testPhantomReference();
    }

    /**
     * 测试强引用
     *
     * @throws InterruptedException
     */
    public static void testStrongReference() throws InterruptedException {
        byte[] StrongReference1, StrongReference2, StrongReference3, StrongReference4;
        StrongReference1 = new byte[1 * _1MB];
        StrongReference2 = new byte[1 * _1MB];
        //到这里由于内存不足,所以虚拟机会自动尝试一次自动GC,但是由于是强引用,无法清除对象,造成OutOfMemoryError异常
        StrongReference3 = new byte[1 * _1MB];
    }

    /**
     * 测试软引用
     *
     * @throws InterruptedException
     */
    public static void testSoftReference() throws InterruptedException {
        ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();
        SoftReference softReference1, softReference2, softReference3, softReference4;
        softReference1 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
        softReference2 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
        //到这里由于内存不足,虚拟机会自动尝试一次自动GC
        softReference3 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
        //执行到这里实际上又GC了一次
        softReference4 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
        System.out.println("第一次GC之后的值");
        System.out.println(softReference1.get());
        System.out.println(softReference2.get());
        System.out.println(softReference3.get());
        System.out.println(softReference4.get());
        System.out.println("===========>");
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        //到这里,尝试手动使虚拟机GC一次,对于软引用,如果内存足够,GC是并不会回收对象的
        System.gc();
        Thread.sleep(500);
        System.out.println("第二次GC之后的值");
        System.out.println(softReference4.get());
        System.out.println("===========>");
        System.out.println(objectReferenceQueue.poll());

    }

    /**
     * 测试弱引用
     *
     * @throws InterruptedException
     */
    public static void testWeakReference() throws InterruptedException {
        ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();
        WeakReference weakReference1, weakReference2, weakReference3, weakReference4;
        weakReference1 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
        weakReference2 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
        //到这里由于内存不足,虚拟机会自动尝试一次自动GC
        weakReference3 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
        //执行到这里实际上又GC了一次
        weakReference4 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
        System.out.println("第一次GC之后的值");
        System.out.println(weakReference1.get());
        System.out.println(weakReference2.get());
        System.out.println(weakReference3.get());
        System.out.println(weakReference4.get());
        System.out.println("===========>");
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        //到这里,尝试手动使虚拟机GC一次,,对于弱引用,即使内存足够,GC还是会回收对象的
        System.gc();
        Thread.sleep(500);
        System.out.println("第二次GC之后的值");
        System.out.println(weakReference4.get());
        System.out.println("===========>");
        System.out.println(objectReferenceQueue.poll());
    }

    /**
     * 测试虚引用
     *
     * @throws InterruptedException
     */
    public static void testPhantomReference() throws InterruptedException {
        ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();

        PhantomReference phantomReference1, phantomReference2, phantomReference3;
        phantomReference1 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
        phantomReference2 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
        System.gc();
        Thread.sleep(500);
        System.out.println(objectReferenceQueue.poll());
        System.out.println(objectReferenceQueue.poll());
        /*按照我们的思维,这是比软引用和弱引用还弱的引用,调用一次GC,虚引用关联的匿名对象会被GC掉,这看起来确实没错,并且已经被加入到了ReferenceQueue中(可以poll得到数据),但实际上在初始化下一个phantomReference3时还是会会抛出OOM异常,就像强引用一样
        问题 就在于这个虚引用.在GC启动时,会将虚引用对象传到它的引用队列中去,这没错.但是却不会将虚引用的referent字段设置成null,
        这样一来,也就不会释放虚引用指向的匿名数组的堆内存空间,看起来这个匿名数组被回收了,但实际上phantomReference的内部的referent已经持有了这个数组,造成了内存泄漏
        使用时一定要注意这个问题*/

        /*我们可以采用下面的操作,手动将PhantomReference的referent置为null,然后再次GC时,这样就会真正的清理内存空间*/

        /*phantomReference1.clear();
        phantomReference2.clear();*/

        phantomReference3 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
    }
}

测试前,我们首先将VM参数设置为:-Xms2M -Xmx3M 表示虚拟机启动内存2M,最大内存3M
在这里插入图片描述
在这里插入图片描述

3.1 虚引用的坑

  软引用、弱引用以及虚引用可以与一个引用队列一起配合使用。
  当弱引用和软引用引用的对象需要进行回收的时候,JVM都是先将其内部的referent字段设置成null,之后将软引用或弱引用引用的对象,加入到关联的引用队列中。也就是说JVM先回收堆对象占用的内存,然后才将软引用或弱引用加入到引用队列。
  而虚引用(PhantomReference) 不同, 他必须和引用队列 (ReferenceQueue)联合使用,若GC启动时,则将引用对象传到它的引用队列中去,但是不会将虚引用的referent字段设置成null, 也不会释放虚引用指向对象的堆内存空间,因此使用虚引用一定要注意内存泄漏。我们可以通过手动调用PhantomReference.clear()方法来释放虚引用指向对象的堆内存空间。

  在与引用队列相关联的类构造器中,都会调用父类Reference的构造器:

/**
 * SoftReference的构造器
 * @param referent
 * @param q
 */
public SoftReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
    this.timestamp = clock;
}

/**
 * WeakReference的构造器
 * @param referent
 * @param q
 */
public WeakReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}

/**
 * PhantomReference的构造器
 * @param referent
 * @param q
 */
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}

/**
 * 父类Reference的构造器
 * @param referent
 * @param queue
 */
Reference(T referent, ReferenceQueue<? super T> queue) {
    //这里将关联对象赋值给referent字段引用
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  在GC之时,软引用、弱引用对象的referent会被置空,并加入引用队列,但是虚引用的referent不会置空,我们使用断点DEBUG测试。
软引用测试:
在这里插入图片描述
我们查看softReference1中的数据,可以看到referent字段已经被置为null

在这里插入图片描述
弱引用测试:

在这里插入图片描述
我们查看weakReference1中的数据,可以看到referent字段已经被置为null
在这里插入图片描述
虚引用测试:
在这里插入图片描述
我们查看phantomReference1中的数据,可以看到referent字段没有被置为null,造成内存泄漏。
在这里插入图片描述
下面来看看clear方法的源码:

public void clear() {
    this.referent = null;
}

  将referent字段置空,这样上面的匿名数组就能真正的被回收了。因此在追踪对象之后要调用clear方法清除引用,但是即使这样做,还是需要下一次GC才能真正清理这对象,这已经算是改变对象的生命周期了。

4 总结

  即便是弱引用和软引用能够自动回收, 但如果虚拟机来不及回收弱引用或软引用指向的对象时也是会抛出 java.lang.OutOfMemoryError: Java heap space 异常的。而虚引用则会明确改变对象的生命周期,甚至造成内存泄漏。
  对于四种引用我们现在认识了一些,但是java.lang.ref包中还有一个FinalReference(继承了Reference)、Finalizer(继承了FinalReference)等类,他们是与可达性分析算法的二次表及相关联的,可以看看这篇文章,具体了解这些类的运作可以看这篇文章:Java中的Finalizer类以及GC二次标记过程中的Java源码解析

发布了58 篇原创文章 · 获赞 105 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/105211090