Java 对象引用方式 —— 强引用、软引用、弱引用和虚引用

Java中负责内存回收的是JVM。通过JVM回收内存,我们不需要像使用C语音开发那样操心内存的使用,但是正因为不用操心内存的时候,也会导致在内存回收方面存在不够灵活的问题。

为了解决内存操作不灵活的问题,我们可以通过了解Java的引用方式来解决这个问题。

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

下面我们来看一下四种级别的引用方式的特点:

1.强引用

我们使用的大部分的引用都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,GC绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果一个对象只具有软引用,在内存空间足够的时候,GC不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要GC没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被GC了,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在GC扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

 

针对软引用的更详细的说明:

详细解释:软引用的行为准则

软引用的核心行为是:当且仅当对象只被软引用指向(没有强引用)时,垃圾回收器才会在内存不足(OutOfMemoryError 抛出之前)的特定情况下考虑回收它。

你可以把不同引用的强度和理解为一个优先级队列:

强引用 (Strong Reference) > 软引用 (Soft Reference) > 弱引用 (Weak Reference)

垃圾回收器的工作逻辑是:

首先标记所有不可达的对象。一个对象不可达意味着从“GC Roots”(如线程栈的局部变量、静态变量等)出发,无法通过一系列强引用链访问到它。

对于这些不可达的对象,垃圾回收器再检查它们被哪些较弱的引用指着,并根据规则决定是否回收:

  • 如果对象有强引用:跳过,绝对不回收。(最高优先级)
  • 如果对象没有强引用,但有软引用:“嗯,这是个缓存对象。现在内存好像有点紧张了,为了不抛OOM,我得把它清理掉。” —— 仅在内存不足时回收。
  • 如果对象没有强引用和软引用,只有弱引用:“这个没用了,不管内存够不够,直接清理掉。” —— 下次GC运行时立即回收。

关键结论:

  • 强引用是对象的“生命线”。只要生命线还在,软引用和弱引用都无法决定对象的生死。

  • 内存不足(GC压力大)是触发只被软引用指向的对象被回收的必要条件,但不是充分条件。那个首要的充分条件是:对象必须已经没有强引用了。

举例说明:

    public static void main(String[] args) {
        // 1. 创建一个强引用对象
        String strongRef = new String("我是受保护的对象");

        // 2. 创建一个指向它的软引用
        SoftReference<String> softRef = new SoftReference<>(strongRef);

        System.out.println("内存充足,且强引用存在时:");
        System.out.println("强引用: " + strongRef);
        System.out.println("软引用: " + softRef.get()); // 能正常获取到对象

        // 模拟内存不足(只是一种演示,实际中很难精确控制)
        try {
            // 疯狂分配内存,消耗堆空间,给GC施加压力
            List<byte[]> list = new ArrayList<>();
            while (true) {
                list.add(new byte[1 * 1024 * 1024]); // 每次分配1MB
            }
        } catch (OutOfMemoryError e) {
            // 预期中会捕获到OOM错误
            System.out.println("\n系统已发生OutOfMemoryError...");
        }

        // 检查GC后在强引用依然存在的情况下,软引用的状态
        System.out.println("\n发生OOM后,强引用依然存在:");
        System.out.println("强引用: " + strongRef); // 对象肯定还在!
        System.out.println("软引用: " + softRef.get()); // 对象也肯定还在!

        // 3. 现在,我们移除强引用
        strongRef = null;

        System.gc();
        System.out.println("\n强引用为空了,触发gc:");
        System.out.println("强引用: " + strongRef); // 对象为null
        System.out.println("软引用: " + softRef.get()); // 对象也肯定还在!

        // 再次模拟内存不足,触发GC
        try {
            List<byte[]> list = new ArrayList<>();
            while (true) {
                list.add(new byte[1 * 1024 * 1024]);
            }
        } catch (OutOfMemoryError e) {
            System.out.println("\n再次发生OutOfMemoryError...");
        }

        // 检查在强引用不存在且内存不足后,软引用的状态
        System.out.println("\n发生OOM后,且强引用已被移除:");
        System.out.println("软引用: " + softRef.get());
        // 这次很可能输出 null!因为对象只剩软引用,且在内存不足时被GC回收了。
    }

输出内容:

内存充足,且强引用存在时:
强引用: 我是受保护的对象
软引用: 我是受保护的对象

系统已发生OutOfMemoryError...

发生OOM后,强引用依然存在:
强引用: 我是受保护的对象
软引用: 我是受保护的对象

强引用为空了,触发gc:
强引用: null
软引用: 我是受保护的对象

再次发生OutOfMemoryError...

发生OOM后,且强引用已被移除:
软引用: null

 

针对弱引用的说明:

    public static void main(String[] args) {
        // 创建一个强引用对象
        Object strongObject = new Object();

        // 创建一个指向同一个对象的弱引用
        WeakReference<Object> weakRef = new WeakReference<>(strongObject);

        System.out.println("强引用还在时:");
        System.out.println("strongObject: " + strongObject);
        System.out.println("weakRef.get(): " + weakRef.get());
        // 两者输出相同的对象地址,证明弱引用可以获取到对象

        // 执行GC(只是一个建议,不保证立即执行)
        System.gc();

        // 稍等片刻,让GC有机会运行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        System.out.println("\nGC后,强引用还在时:");
        System.out.println("strongObject: " + strongObject); // 对象还在
        System.out.println("weakRef.get(): " + weakRef.get());
        // 弱引用获取到的对象也还在!因为强引用还在守护着它。

        // 现在,我们去掉强引用
        strongObject = null; // 关键步骤!对象现在只剩弱引用指向它了

        // 再次执行GC
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        System.out.println("\nGC后,强引用被置为null时:");
        System.out.println("weakRef.get(): " + weakRef.get());
        // 输出 null!因为对象只剩弱引用,已被GC回收。
    }
强引用还在时:
strongObject: java.lang.Object@5305068a
weakRef.get(): java.lang.Object@5305068a

GC后,强引用还在时:
strongObject: java.lang.Object@5305068a
weakRef.get(): java.lang.Object@5305068a

GC后,强引用被置为null时:
weakRef.get(): null

 

针对虚引用的补充说明:

  • 不会回收:内存不足且强引用存在时,对象绝不会被回收,其虚引用也安然无恙。

  • 核心特性:虚引用的 get() 方法总是返回 null,它的存在完全不影响对象的生命周期。

  • 唯一用途:与 ReferenceQueue 配合,作为一种事后通知机制,用于在对象被真正销毁后执行一些高级的清理工作(通常是Java堆之外资源的清理)。

  • 执行顺序:finalize() 方法执行 -> 对象内存被回收 -> 虚引用被加入队列。这意味着虚引用提供了一种比 finalize() 方法更可靠、更灵活的对象生命周期跟踪机制。 

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

public class PhantomReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个强引用对象和一个引用队列
        Object strongObject = new Object();
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        
        // 创建虚引用,并关联队列
        PhantomReference<Object> phantomRef = new PhantomReference<>(strongObject, queue);
        
        System.out.println("1. 强引用还在,内存充足与否都无关紧要:");
        System.out.println("strongObject: " + strongObject);
        System.out.println("phantomRef.get(): " + phantomRef.get()); // 永远是 null
        System.out.println("queue.poll(): " + queue.poll()); // 队列是空的
        
        // 模拟内存不足(同样,只是为了演示GC压力)
        tryToFillHeap();
        
        System.out.println("\n2. 即使模拟了内存不足,强引用还在:");
        System.out.println("strongObject: " + strongObject); // 对象还在
        System.out.println("phantomRef.get(): " + phantomRef.get()); // 依然是 null
        System.out.println("queue.poll(): " + queue.poll()); // 队列依然是空的
        
        // !!!关键步骤:移除强引用 !!!
        strongObject = null;
        
        // 建议GC,并给它一点时间运行和排队
        System.gc();
        Thread.sleep(500); 
        
        System.out.println("\n3. 强引用被移除后,并发生了一次GC:");
        System.out.println("phantomRef.get(): " + phantomRef.get()); // 永远是 null
        
        // 现在检查队列:虚引用对象本身被加入了队列,这意味着它监控的对象已被销毁
        Object queuedRef = queue.poll();
        System.out.println("queue.poll(): " + queuedRef);
        if (queuedRef != null) {
            System.out.println(">>> 通知:虚引用指向的原始对象已被回收。可以在这里执行清理工作。 <<<");
        }
    }
    
    private static void tryToFillHeap() {
        // 尝试消耗内存,可能成功也可能不成功,取决于JVM配置
        try {
            List<byte[]> list = new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                list.add(new byte[1024 * 1024]); // 1MB
            }
        } catch (OutOfMemoryError e) {
            // 忽略,预期中
        }
    }
}

 

针对各类引用的说明:核心在于:强引用的存在是对象存活的绝对保证,其他所有引用类型的行为都是基于“当强引用消失后”这一前提的。

 

 

Android 开发中会使用到弱引用或者软引用的地方

1.解决Handler可能造成的内存泄露 -- 使用弱引用

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,不然你怎么可能通过Handler来操作Activity中的View。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。

然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。

2.解决图片加载时,可能造成的内存不足问题 -- 使用软引用

使用软引用相对使用强引用,在图片加载方面能够很明显的提升性能,并减少崩溃的几率,与Lru算法指定LruCache能够更好的去管理,因为增加了根据图片使用频率来管理内存的算法,相比较更加合理和人性化。

 

 

posted @ 2016-11-16 19:38  灰色飘零  阅读(6539)  评论(0)    收藏  举报