Java引用类型

时间:2024-03-22 14:02:28

1. 前序

在了解引用的分类之前先了解一下对象和对象引用之间的区别。

1.1 对象

        对象是类的实例。当使用new关键字创建类的实例时,jvm 会在堆内存中给该对象分配内存空间。
        对象的特性:
        ① java对象普遍存储在堆内存中(其他情况,在经过【即时编译器】的【逃逸分析】技术分析之后,如果 能够确定一个对象不会逃逸到线程之外,那么就可以在 虚拟机栈 上为这个对象分配内存,这被称为【栈上分配】)。
        ② java对象 包含实例变量(非静态字段)和方法。

举例,假设有一个名为 Person的类,那么创建一个对象的行为可以是:

Person person = new Person();

在这行代码中,new Person() 实际上就是创建了一个 Person类型的对象。

1.2 对象引用

        对象引用 就是一个变量,它存储的是 Java对象在堆内存中的地址。当你声明了一个变量用来存储对象时,你创建的就是一个引用,这个引用指向了堆内存中的对象,你可以通过这个引用来访问对象。
        对象引用的特性:
        ① 对象引用可以存储在堆内存,也可以存储在虚拟机栈内存中(作为局部变量存储时)。
        ② 对象引用指向堆内存中的一个对象实例。
        ③ 如果改变指向,则就指向堆内存中的另一个不同的对象。
        ④ 如果引用被设置为 null,它就不再指向任何对象,这样做可以让原本引用的对象成为垃圾回收的候选对象。

        继续上面的例子,变量 person 就是一个对象引用,它指向 new Person() 创建的 Person对象实例。概括来说,对象时具体的数据实体,而对象引用相当于指针或者句柄,指向这些实体数据。

2. 引用

2.1 引用的分类

Q:在Java中,对象的引用分为了四个类别:强引用、软引用、弱引用、虚引用。为什么要分这么多类型的引用?

A:这些引用类型提供了不同的生命周期和垃圾收集行为,Java虚拟机在进行垃圾回收时,可以根据应用场景的需要选择性的回收对象。

2.2 强引用

举例

Person person = new Person();

        强引用在编程中最常见。只要 对象有强引用指向它,垃圾回收器就不会回收这个对象。当 person变量超出作用域或者被显示的设置为null时(代表着对象已经不可达),才会被gc回收。只要 person这个变量在作用域内,new Person()创建的对象就不会被GC回收,无论内存状况如何,因为它是可达的(垃圾标记算法:可达性分析算法)。

2.3 软引用

举例

SoftReference<Person> softRef = new SoftReference<>(new Person());

        软引用是一种对内存敏感的引用类型。在内存充足时,软引用指向的对象 就不会被回收。在内存不足时,可能会被回收。
        软引用通常用来实现内存敏感的缓存。

2.4 弱引用

举例

WeakReference<Person> weakRef = new WeakReference<>(new Person());

        弱引用的生命周期比软引用更短。在JVM垃圾回收时,只要发现了软引用,不管当前内存空间是否足够,都会回收这个对象。

2.4.1 弱引用与强引用

结论:【Weak Reference 允许对象在没有强引用指向的情况下被垃圾收集器回收】。

解释:

        这句话意味着,当一个对象只通过 WeakReference(弱引用)被引用时,它可以被 Java垃圾收集器(GC)回收,即便这个弱引用本身还存在。在Java中,一个对象是否可以被回收主要取决于它们是否可达。

举例

WeakReference<Person> weakRef = new WeakReference<>(new Person());

        一旦上述代码执行过后,如果没有其他地方通过强引用指向 new Person()创建的对象,那么该对象就只剩下了一个弱引用weakRef。在这种情况下,即使 weakRef 还在作用域内,垃圾收集器 在下一次执行GC的时候也会认为这个对象是可以被回收的,因为它不是通过强引用可达的。

部分代码示例如下:

public static void weakReferenceTest() throws InterruptedException {
    // 强引用。new Object() 会创建一个对象,变量 srObj 就是对象的强引用。
    Object srObj = new Object();

    // 弱引用。变量 weakReference 就是对象的弱引用。
    WeakReference<Object> weakReference = new WeakReference<>(srObj);
    // 输出 srObj 指向的 Object对象实例的地址
    System.out.println("前:" + weakReference.get());

    // 将强引用设置为null。此时,srObj 原本指向的 Object对象实例就可以被gc回收了。
    // 因为当一个对象只有弱引用指向它时,这个对象就可以被回收了,即便弱引用weakReference还存在。
    srObj = null;
    // 执行垃圾收集,模拟jvm自动垃圾回收动作。实际情况是无法预知gc具体什么时间执行。
    System.gc();
    // 给 gc 一些时间来清理 弱引用
    Thread.sleep(2000);

    // 因为当一个对象只有弱引用指向它时,这个对象就可以被回收了。所以,这里只会输出null。
    System.out.println("后:" + weakReference.get());
}

2.4.2 WeakHashMap

        弱引用通常用来实现 “不阻碍对象被回收的关联映射”。

        “关联映射” 通常指的是 键值对的存储结构。比如HashMap,在常规的HashMap中,一旦你添加了键值对,它们就会持续存在于映射中,直到你显式的移除了它们。这种情况下,即使外部没有其他的强引用指向这个键或者值,它们也不会被垃圾回收,因为HashMap自身维持着对它们的强引用。
        但是,如果我们想要 允许垃圾收集器 根据需要自动移除不再使用的对象,来避免内存泄漏,这个时候就可以使用弱引用。比如,WeakHashMap,它内部就使用了弱引用。WeakHashMap的键是弱引用,这就意味着一旦外部对键的强引用不存在,这个键就可以被垃圾收集器回收了。如果某个键被回收了,那么这个键对应的键值对也会从WeakHashMap中自动移除。

部分代码示例如下:

public static void weakHashMapTest() throws InterruptedException {
    // 1- 创建一个 weakHashMap
    WeakHashMap<TbAddress, String> weakHashMap = new WeakHashMap<>();

    // 2-1 创建一个键的强引用。
    TbAddress sfTa = new TbAddress();
    // 将 键和值 放入 weakHashMap中。
    weakHashMap.put(sfTa, "123");
    // 2-2 再创建两个键值对,没有强引用指向它们的键(只有WeakHashMap中的弱引用指向它们的键)
    weakHashMap.put(new TbAddress(), "456");
    weakHashMap.put(new TbAddress(), "789");

    /**
     * 3- 执行垃圾收集,垃圾收集器 可能会 回收没有 被强引用指向的键。
     *    这个过程不确定,因为GC的确切行为取决于诸多因素。比如,GC的算法、系统的内存状态、JVM的配置等。这里只是为了促进gc的执行,只是为了演示,实际情况是无法预知gc具体什么时间执行。
     */
    System.gc();

    // 4- 给 gc 一些时间来清理弱引用
    Thread.sleep(2000);

    // 5- 打印 WeakHashMap 中仍然存在的键值对。如果发生了gc,那么这里就只会输出 "123"。
    weakHashMap.forEach((tbAddress, s) -> System.out.println(s));
}

2.5 虚引用

举例

PhantomReference<Person> phantomRef = new PhantomReference<>(new Person(), someReferenceQueue);

        虚引用是最弱的一种引用,【虚引用也不会影响其引用的对象生命周期】。
        通过 PhantomReference的get()方法始终会返回null,这意味着,不能通过虚引用来获取对象实例。
        虚引用主要用来 跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(ReferenceQueue)结合起来使用。【当垃圾回收准备回收对象时,如果这个对象有与之关联的虚引用,并且这个 虚引用已经被注册到了某个 ReferenceQueue(引用队列)上,那么这个 虚引用 就会被添加到 与之关联的 ReferenceQueue中,这个过程由 gc 自行完成】。
        虚引用 会用来做一些 对象被回收之前的清理工作,比如,释放资源、性能监控与分析等。

部分伪代码示例如下:

public static void phantomReferenceTest() throws InterruptedException {
    // 强引用。new Object() 会创建一个对象,变量 srObj 就是 new Object()创建的对象实例 的强引用。
    Object srObj = new Object();
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    // 虚引用。变量 phantomReference 就是 srObj强引用所指向的 new Object()对象实例 的虚引用。
    PhantomReference<Object> phantomReference = new PhantomReference<>(srObj, referenceQueue);

    // 虚引用不能通过正常的方法来获取对象实例。所以,下面会输出null。
    System.out.println("phantomReference Get: " + phantomReference.get());
    // 下面输出 false,因为 上面的 srObj强引用所指向的 new Object()对象实例 还没有被gc回收,所以,虚引用也不会被加入到引用队列中。
    System.out.println("phantomReference 是否已入队(引用队列)" + phantomReference.isEnqueued());

    // 清除强引用,让对象符合GC回收的条件。
    srObj = null;
    // 强制执行垃圾收集,模拟jvm自动垃圾回收动作。实际情况是无法预知gc具体什么时间执行。
    System.gc();
    // 给 gc 一些时间来清理 弱引用
    Thread.sleep(2000);

    boolean enqueued = phantomReference.isEnqueued();
    System.out.println("phantomReference 是否已入队(引用队列)" + enqueued);
    if (enqueued) {
        // do something like cleanup resources here

        // 你可以使用 poll() 或者 remove()方法 从引用队列中获取 虚引用
        PhantomReference<Object> reference = (PhantomReference) referenceQueue.poll();
        if (null != reference) {
            System.out.println(reference);
        }
    }
}