一、先说清:JVM 里的“引用类型”到底在干嘛?

Java 里我们常说的 强引用 / 软引用 / 弱引用 / 虚引用,本质是在告诉 GC:

  • 这个对象“有多重要”
  • 什么时候允许回收它
  • 回收前后我能不能得到通知(ReferenceQueue)

友情提示:System.gc() 不是强制回收,但我们可以用「ReferenceQueue + 重试循环 +(必要时)压内存」让结果非常稳定、肉眼可见。


二、强引用(Strong Reference)

1、定义

最普通的赋值就是强引用:

A a = new A();

只要对象还能从 GC Roots(栈、静态变量、JNI 引用等)沿引用链走到,它就不会被回收。

2、如何“让它能被回收”

把强引用断开即可:

a = null;

3、Demo:看见“对象被回收”的结果(用 finalize 只是演示,生产别用)

finalize() 已被官方不推荐,但用来做“看见回收发生”的演示很直观。

public class StrongRefDemo {
    static class A {
        private final String name;
        A(String name) { this.name = name; }
        @Override protected void finalize() throws Throwable {
            System.out.println("[finalize] A(" + name + ") 被回收了");
        }
    }

    public static void main(String[] args) throws Exception {
        A a = new A("strong");
        System.out.println("创建对象: " + a);

        a = null;                 // 断开强引用
        System.gc();              // 建议触发回收
        Thread.sleep(200);        // 给 GC 一点时间

        System.out.println("main 结束");
    }
}

你通常会看到类似输出(是否立刻打印取决于 JVM,但一般能看到):

在这里插入图片描述

更严谨的“可观测”方式是用 WeakReference/ReferenceQueue 来判断对象是否死亡(见后面弱引用 Demo)。


三、软引用(SoftReference)

1、定义

SoftReference<A> ref = new SoftReference<>(new A());

2、回收规则(重点)

  • 只有软引用指向对象时:内存充足通常不回收
  • 内存紧张时:GC 会倾向回收软引用对象(常用于缓存)

3、Demo:压内存,稳定看到“软引用被清空”

下面 Demo 会不断申请内存,直到软引用被回收(ref.get() == null),并用 ReferenceQueue 看到它入队。

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

public class SoftRefDemo {
    static class A {
        final byte[] payload = new byte[5 * 1024 * 1024]; // 5MB
    }

    public static void main(String[] args) {
        ReferenceQueue<A> queue = new ReferenceQueue<>();
        SoftReference<A> ref = new SoftReference<>(new A(), queue);

        System.out.println("初始 ref.get() = " + ref.get());

        // 压内存:不停分配大块数组
        List<byte[]> pressure = new ArrayList<>();
        try {
            for (int i = 1; ; i++) {
                pressure.add(new byte[10 * 1024 * 1024]); // 每次 10MB
                if (i % 5 == 0) {
                    System.out.println("已分配约 " + (i * 10) + "MB, ref.get() = " + ref.get());
                }

                if (ref.get() == null) {
                    System.out.println("\n✅ 软引用对象已被回收:ref.get() == null");
                    break;
                }
            }
        } catch (OutOfMemoryError e) {
            System.out.println("\n⚠️ 内存压爆了,但 ref.get() 仍为: " + ref.get());
        }

        Reference<? extends A> polled = queue.poll();
        System.out.println("ReferenceQueue 是否收到通知: " + (polled != null));
    }
}

运行时你会看到类似:

在这里插入图片描述


四、弱引用(WeakReference)

1、定义

WeakReference<A> ref = new WeakReference<>(new A());

2、回收规则(重点)

  • 只有弱引用指向对象时:只要发生 GC,就会回收(一般非常快)
  • 典型用途:ThreadLocalMap 的 Entry key

3、Demo:稳定看到“弱引用被清空 + 入队”

这个 Demo 基本 99% 都能稳定看到结果。

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class WeakRefDemo {
    static class A {
        final String name;
        A(String name) { this.name = name; }
        @Override public String toString() { return "A(" + name + ")"; }
    }

    public static void main(String[] args) throws Exception {
        ReferenceQueue<A> queue = new ReferenceQueue<>();
        WeakReference<A> ref = new WeakReference<>(new A("weak"), queue);

        System.out.println("GC 前 ref.get() = " + ref.get());

        // 多试几次,保证你能“看见”结果
        for (int i = 1; i <= 20 && ref.get() != null; i++) {
            System.gc();
            Thread.sleep(50);
        }

        System.out.println("GC 后 ref.get() = " + ref.get() + "(null 表示已回收)");

        Reference<? extends A> polled = queue.poll();
        System.out.println("ReferenceQueue 是否收到通知: " + (polled != null));
    }
}

在这里插入图片描述


五、虚引用(PhantomReference)

1、定义

虚引用有两个特点:

  • 必须配合 ReferenceQueue 使用
  • get() 永远返回 null(你拿不到对象)
PhantomReference<A> ref = new PhantomReference<>(new A(), queue);

2、用途(重点)

用来做“对象即将被回收”的通知,典型用法:

  • Cleaner 清理 DirectByteBuffer 的直接内存(非堆内存)
  • 资源释放回调(更可控的 finalize 替代思路)

3、Demo:看到“虚引用入队”(这就是回收信号)

我们用一个大对象 + 循环触发 GC,等虚引用入队就说明 referent 已不可达并进入回收流程。

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

public class PhantomRefDemo {
    static class A {
        final byte[] payload = new byte[20 * 1024 * 1024]; // 20MB
        final String name;
        A(String name) { this.name = name; }
        @Override public String toString() { return "A(" + name + ")"; }
    }

    public static void main(String[] args) throws Exception {
        ReferenceQueue<A> queue = new ReferenceQueue<>();

        A a = new A("phantom");
        PhantomReference<A> ref = new PhantomReference<>(a, queue);

        System.out.println("ref.get() 永远是: " + ref.get());

        a = null; // 断开强引用

        // 等待入队:入队意味着对象已被判定为可回收(回收阶段会通知)
        Reference<? extends A> polled = null;
        for (int i = 1; i <= 50 && polled == null; i++) {
            System.gc();
            Thread.sleep(50);
            polled = queue.poll();
        }

        System.out.println("虚引用是否入队(true 表示回收信号已到): " + (polled != null));
    }
}

在这里插入图片描述


六、补充:ThreadLocal 为什么用“弱引用”?和强/弱引用有什么关系?

ThreadLocalMap 的 Entry 设计里(概念上):

  • key(ThreadLocal)是弱引用
  • value 是强引用

这样做的目的之一是:如果 ThreadLocal 这个 key 在外部不再被引用了,GC 可以把 key 回收,避免 key 永远挂在线程里。

但问题来了:

  • key 被回收后,Entry 可能变成 key=null
  • value 仍是强引用,仍然被线程的 ThreadLocalMap 持有
  • 线程池线程长期不结束 → value 残留更久

所以你会经常听到一句最佳实践:

ThreadLocal 用完必须 remove():既防止线程池“串台”,也更利于清理残留 value。


七、小结

  • 强引用:有强引用就不会回收;断开引用才可能回收
  • 软引用:内存够时不回收;内存紧张才回收(适合缓存)
  • 弱引用:只要发生 GC 就回收(适合“不要阻止回收”的引用)
  • 虚引用:拿不到对象(get 永远 null),配合队列做回收通知(资源清理)
  • ThreadLocalMap:key 弱引用、value 强引用 → 务必 remove()
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐