Java 四种引用类型最详解

1,143 阅读7分钟

Java对象的引用包括四种:

由强到弱依次

强引用 -> 软引用 -> 弱引用 -> 虚引用

Java中提供这四种引用类型主要有两个目的:

  1. 可以让程序员通过代码的方式决定某些对象的生命周期;

  2. 有利于JVM进行垃圾回收。

前置知识点

Java垃圾回收机制

为什么要回收垃圾?

在平时的开发当中,有时候我们需要创建大量的对象,如果我们动态创建的对象没有得到及时回收,持续堆积,最后会导致内存被占满,造成溢出

因此Java 提供了一种垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把内存的垃圾全部进行回收,从而保证程序的正常运行。

Java如何判断哪些对象是垃圾?

  1. 引用计数法

此方法中,堆中的每个对象都会添加一个引用计数器。每当一个地方引用这个对象时,计数器值 +1;当引用失效时,计数器值 -1。任何时刻计数值为 0 的对象就是不可能再被使用的。

  1. 可达性分析法 可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。

Java内存泄漏

指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,Java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度

强引用

什么是强引用?

Java 默认就是强引用, 通过 new 建立对象, 得到的引用都是强引用, 也就是日常开发中接触到的都是强引用.

例如:

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

强引用的特点

在强引用依旧被持有的时候, 不会被Jvm的垃圾回收进行回收.

这也是Java能够使用垃圾回收机制的前提, 不然代码还在使用某项, 就给把对象回收了, 肯定就会出问题了

引用被持用

引用被持有: 可以简单理解为有一段正在运行(或者线程阻塞, 等待等状态,运行只是一种形象的说法)的代码, 其中包含了某个对象A的引用, 那么在这段代码执行完之前, 对象A的引用就一直被持有.(在垃圾回收机制中 引用计数法或者可达性分析法判断某个对象依旧是否被引用)

public class Main {

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Object objectRef = new Object();

        for (int i = 0; i < 100000; i ++) {
            System.out.println(i);
        }

    }
}

例如上面代码, 在fun1方法运行完之前, objectRef这个引用一直被持有, 在这段时间内进行的垃圾回收操作都不会回收objectRef对象.

释放引用

但是如果我的代码某个方法很长, 某个对象后续已经不会用到了, 怎么让它能提前被回收呢?

通过设置objectRef = null就可以释放该引用

public class Main {

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Object objectRef = new Object();

        for (int i = 0; i < 100000; i ++) {
            if (i == 500) {
                objectRef = null;
            }
            System.out.println(i);
        }


    }
}

缺点

代码如果设计的有问题, 强引用会带来内存泄漏

软引用

什么是软引用?

通过new SoftReference(T)的形式得到SoftReference类的引用(将一个引用指向一个对象)

例如:

SoftReference softReference = new SoftReference(new Object());

软引用的特点

如果一个对象只有软引用,内存空间足够,垃圾回收机制就不会回收它.如果内存空间不足了,就会回收软引用包含的强引用。前提是只有软引用指向.

只要垃圾回收器没有回收软引用所包含的强应用,该软引用就可以被程序使用。

如何使用软引用

通过get()方法获取软引用包含的强引用来使用
public class Main {

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        SoftReference softReference = new SoftReference(new Dog());
        for (int i = 0; i < 100000; i ++) {
            System.out.println(i);
        }
        // 如果在get()方法之前,jvm因为内存不足已经将softReference回收
        // 那么 get()方法返回null
        Dog dog = (Dog) softReference.get();
        if (dog != null) {
            dog.bark();
        }
    }
}

注意,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

使用软引用能防止内存泄露,增强程序的健壮性。

缺点

软引用被Java垃圾回收 回收的只是其指向的对象, 但是软引用对象本身是不会被回收的, 就使得有一部分内存依旧会被浪费.可以通过ReferenceQueue来构造软引用对象, 以解决这个问题

例如:

public class Main {

    private ReferenceQueue referenceQueue = new ReferenceQueue();

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        SoftReference softReference = new SoftReference(new Dog(), referenceQueue);
        // 当softReference指向的对象被垃圾回收后, softReference对象就被添加到队列referenceQueue中
        // 因此 我们可以定期判断referenceQueue是否为空, 如果不为空, 定期清楚队列中的软引用对象
        SoftReference ref = null;
        while ((ref = (SoftReference) referenceQueue.poll()) != null) {
            // 释放该软引用
            ref = null;
        }

    }
}

应用

软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。

弱引用

什么是弱引用

通过new WeakReference(T)的形式得到WeakReference类的引用(将弱引用指向一个对象)

弱引用的特点

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,如果一个对象只有弱引用指向它, 那么无论内存是否充足,都会回收弱引用指向的对象. 但是如果该对象除了弱引用以外, 还有别的引用指向它(例如强引用), 那么jvm会根据情况决定是否回收.

public class Main {

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        WeakReference weakReference = new WeakReference(new Dog());
        System.out.println(weakReference.get());
        // 调用jvm强制gc一次
        System.gc();
        // 此时弱引用包含的强引用为null
        System.out.println(weakReference.get());
    }
}

缺点

和软引用的缺点相同, 同样可以通过ReferenceQueue解决

WeakReference weakReference = new WeakReference(new Dog(), referenceQueue);

应用

在应用时, 通常是使用java 中的weakHashMap类, WeakHashMap和HashMap几乎一样,唯一的区别就是它的键(不是值!!!)使用WeakReference引用。当WeakHashMap的键标记为垃圾的时候,这个键对应的条目就会自动被移除。

即如果key的没有任何其它引用的时候, 就会回收这一对key-value

虚引用

什么是虚引用

通过new PhantomReference(T, referenceQueue)的形式得到PhantomReference类的引用(通过一个强引用得到一个虚引用)

虚引用必须和引用队列referenceQueue一起使用

虚引用的特点

虚引用和前面的软引用、弱引用不同,一个对象是否有虚引用的存在,完全不会对其生命周期构成影响(其生命周期由自身的引用类型决定),也无法通过虚引用get()方法获得一个对象实例。

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

public class Main {

    private ReferenceQueue referenceQueue = new ReferenceQueue();

    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Dog dog = new Dog();
        System.out.println("强引用:" + dog);
        PhantomReference phantomReference = new PhantomReference(dog, referenceQueue);
        System.out.println("虚引用:" + phantomReference);
        // 虚引用get()方法得到的永远是null
        System.out.println("虚引用包含的强引用:" + phantomReference.get());
        // 通过弱引用控制对象的生命周期
        WeakReference weakReference = new WeakReference(dog);
        // 释放强引用
        dog = null;
        // 强制gc
        System.gc();
        System.out.println("弱引用包含的强引用:" + weakReference.get());
        // 采用特殊方式获取虚引用包含的强引用
        Reference reference = referenceQueue.poll();
        if (reference != null) {
            try {
                Field referent = Reference.class.getDeclaredField("referent");
                referent.setAccessible(true);
                System.out.println("虚引用包含的强引用" + referent.get(reference));
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

    }
}

缺点

和软引用, 虚引用的缺点相同, 同样可以通过ReferenceQueue解决

应用

虚应用可以用来感知一个java对象被回收的时机