05. 引用类型

175 阅读8分钟

Java中有几种引用关系,它们的区别是什么?

在Java中,内存管理非常重要,特别是对于垃圾回收器(Garbage Collector)而言。为了灵活地管理内存,Java提供了四种不同的引用类型:强引用、软引用、弱引用虚引用。每种引用类型在不同的内存管理场景中有不同的作用,下面是它们的详细解释:

1. 强引用(Strong Reference)

定义:强引用是Java中最常见的引用类型。当我们使用new关键字创建一个对象时,默认情况下,我们得到的就是一个强引用。本质就是对象的地址

特点

  • 只要一个对象存在强引用,垃圾回收器(GC)就不会回收这个对象。这意味着对象不会被垃圾回收,即使内存不足也不会回收。

  • 在Java代码中,强引用的表现形式非常简单,通常就是直接使用对象的实例。例如:

    Object obj = new Object(); // obj是一个强引用
    

使用场景:几乎所有的对象引用在默认情况下都是强引用。它们被广泛用于普通对象的引用和方法调用。

2. 软引用(Soft Reference)

定义:软引用是对内存友好的引用类型。如果一个对象只被软引用引用,当内存不足时,垃圾回收器会回收这个对象。它可以用于设计缓存机制。

特点

  • 软引用对象只有在系统内存不足时才会被回收。换句话说,当内存足够时,软引用对象可以一直存在。
  • 软引用在JDK 1.2中被引入,通常用于缓存场景,这样可以确保在内存紧张时,缓存对象可以被回收。

示例

public void testSoftReference() {
    User user = new User(1, "Jett");
    SoftReference<User> userSoft = new SoftReference<>(user); // 创建一个软引用
    user = null; // 断开强引用

    System.out.println(userSoft.get()); // 输出: User对象
    System.gc(); // 显式触发GC
    System.out.println("After gc");
    System.out.println(userSoft.get()); // 如果内存足够,对象不会被回收

    // 模拟内存紧张
    try {
        List<byte[]> list = new LinkedList<>();
        for (int i = 0; i < 100; i++) {
            list.add(new byte[1024 * 1024 * 1]); // 分配大量内存
        }
    } catch (Throwable e) {
        System.out.println("Exception: " + userSoft.get()); // 由于内存不足,软引用对象可能被回收
    }
}

使用场景:软引用通常用于实现缓存。当内存足够时,缓存的对象可以被保留,以提高性能;当内存不足时,缓存对象会被回收,以释放内存。

3. 弱引用(Weak Reference)

定义:弱引用是比软引用更弱的一种引用类型。如果一个对象只被弱引用引用,那么无论当前内存是否足够,一旦垃圾回收器运行,这个对象都会被回收。

特点

  • 弱引用在GC运行时会立即回收弱引用对象,无论内存是否紧张。
  • 弱引用适合用在那些需要偶尔访问但又不必长时间保留的对象,例如在哈希表中清除无用的键值对。

示例

public void testWeakReference() {
    User user = new User(1, "Jett");
    WeakReference<User> userWeakReference = new WeakReference<>(user); // 创建一个弱引用
    user = null; // 断开强引用

    System.out.println(userWeakReference.get()); // 输出: User对象
    System.gc(); // 显式触发GC
    System.out.println("After gc");
    System.out.println(userWeakReference.get()); // GC后对象被回收,输出: null
}

使用场景:弱引用非常适合用在一些内存敏感的结构中,例如WeakHashMap,它可以让JVM自动回收不再使用的对象,以防止内存泄漏。

4. 虚引用(Phantom Reference)

虚引用(Phantom Reference)是一种非常特殊的引用类型,它不像强引用、软引用或弱引用那样直接参与对象的生命周期管理,而只是用于监控对象的回收行为。

虚引用的一个核心作用就是允许我们在对象被垃圾回收后执行某些操作,例如释放相应的资源。这在处理大对象或一些需要延迟释放资源的场景中非常有用。

虚引用的特点与行为

  • 不影响对象的生命周期:虚引用不会阻止对象被垃圾回收。换句话说,即使对象还持有虚引用,只要没有其他强、软、弱引用指向该对象,垃圾回收器就会回收它。
  • 不能通过虚引用获取对象实例:调用虚引用的get()方法总是返回null,因为虚引用的作用并不是为了引用对象,而是为了跟踪对象的垃圾回收状态。
  • 必须与ReferenceQueue配合使用:虚引用通常会与ReferenceQueue(引用队列)一起使用。当垃圾回收器准备回收某个对象时,会将与该对象关联的虚引用加入到引用队列中。这允许程序员通过引用队列得知对象已经被回收,并可以执行后续操作。

应用场景:释放大对象的资源

虚引用在某些需要精确掌握对象回收时间的场景中非常有用。例如,在处理占用大量内存或系统资源的对象时,通常不仅需要释放对象本身,还需要释放与该对象关联的外部资源(如文件句柄、数据库连接等)。

假设我们有一个占用大量内存的大对象(如一个缓存或某个占用大量内存的图像对象),在对象被回收时,我们可能希望做以下操作:

  • 释放关联的系统资源:大对象可能在其生命周期中占用了大量的系统资源,例如文件描述符、网络连接、图形资源等。通过虚引用,我们可以在对象回收后释放这些资源,确保它们不会被长期占用。
  • 进行清理工作:虚引用可以用来触发一些清理操作,例如将数据从缓存中移除,或者执行一些延迟清理的操作。

示例:虚引用用于大对象回收后的资源释放

以下是一个使用虚引用跟踪大对象回收的简单例子:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个引用队列
        ReferenceQueue<BigObject> referenceQueue = new ReferenceQueue<>();

        // 创建一个大对象,并将其与虚引用和引用队列关联
        BigObject bigObject = new BigObject();
        PhantomReference<BigObject> phantomReference = new PhantomReference<>(bigObject, referenceQueue);

        // 将大对象设置为null,断开强引用
        bigObject = null;

        // 触发垃圾回收
        System.gc();
        
        // 检查引用队列,看看大对象是否已经被回收
        if (referenceQueue.poll() != null) {
            System.out.println("大对象已被GC回收,可以执行相关资源释放操作。");
            // 在这里我们可以释放与大对象关联的外部资源
        } else {
            System.out.println("大对象尚未被GC回收。");
        }
    }
}

class BigObject {
    // 模拟一个占用大量资源的大对象
    byte[] bigArray = new byte[1024 * 1024 * 10]; // 10MB大小的数组

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("BigObject被回收");
    }
}

在这个例子中:

  • BigObject 是一个模拟的大对象,它占用了10MB的内存。
  • 我们使用一个 虚引用 关联这个大对象,并将虚引用加入到 引用队列 中。
  • 当我们触发垃圾回收时,如果大对象被回收,虚引用会进入引用队列。
  • 我们可以通过检查引用队列,得知对象是否已被回收,并在回收后执行必要的资源释放操作。

为什么虚引用适用于资源释放?

虚引用允许我们精确掌握对象的回收时间,并且不会影响对象的生命周期。它通常用于以下场景:

  1. 延迟清理:对象被回收后,可能需要做一些延迟清理工作。例如,关闭文件流、释放网络资源或清理缓存。虚引用的回收通知机制允许我们在对象真正被回收后进行这些操作。
  2. 管理外部资源:对于占用大量内存或系统资源的对象,单靠垃圾回收器释放内存并不够。我们需要在对象被回收后手动释放这些外部资源。虚引用可以充当回收的通知机制,帮助我们在回收后清理这些资源。

小总结

  • 虚引用 是Java中最弱的引用类型,它不会影响对象的生命周期。虚引用的主要作用是用来跟踪对象的垃圾回收状态。
  • 虚引用必须与 引用队列ReferenceQueue)配合使用,当对象被回收时,虚引用会被放入引用队列中,从而通知我们该对象已经被回收。
  • 虚引用特别适合用于 监控大对象的回收,并在对象回收后释放与之关联的外部资源。这对于需要精确掌握对象回收时间的场景(例如缓存清理、文件关闭、资源释放等)非常有用。

总结

Java中的四种引用类型各有用途,适用于不同的内存管理场景:

  1. 强引用:最常见,任何时候都不会被GC回收。
  2. 软引用:适用于缓存,在内存不足时被GC回收。
  3. 弱引用:适用于防止内存泄漏的场景,在GC时随时可能被回收。
  4. 虚引用:主要用于跟踪对象的垃圾回收活动,通常配合引用队列使用。