详解Java中的对象的引用和引用级别

标签: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/60/ )。

通常,我们创建一个对象后,会直接引用它,这种引用是强引用。而Java还提供多种级别的引用方式,供我们根据不同的场景使用。本节,我们将介绍Java中的对象引用级别。

此外,我们还将介绍一个有用的引用监控类ReferenceQueue,它将帮我们记录哪些对象已经被回收。

1 Java对象的引用级别

在Java程序的运行过程中,JVM会自动地帮我们进行垃圾回收操作以避免无用的对象占用内存空间。这个过程主要分为两步:

  1. 找出所有的垃圾对象
  2. 清理找出的垃圾对象

我们这里重点关注第一步,即如何找出垃圾对象。这里的关键问题在于如何判断一个对象是否为垃圾对象。

判断一个对象是否为垃圾对象的方法主要有引用计数法和可达性分析法,JVM采用的是可达性分析法。

可达性分析是指JVM会从垃圾回收的根对象(Garbage Collection Root, 简称GC Root)为起点,沿着对象之间的引用关系不断遍历。最终能够遍历到的对象都是有用的对象,而遍历结束后也无法遍历到的应用便是垃圾对象。

根对象不止一个,例如栈中引用的对象、方法区中的静态成员等都是常见的根对象。

我们举一个例子。如果下图中的对象c不再引用对象d,则通过GC Root便无法到达对象d和对象f,那么对象d和f便成了垃圾对象。

图 19-1 可达性分析法示例

有一点要说明,在上图中我们只绘制了一个GC Root,实际在JVM中有多个GC Root。当一个对象无法通过任何一个GC Root遍历到时,它才是垃圾对象。

不过上图展示的这种引用关系是有局限性的。试想存在一个非必须的大对象,我们希望系统在内存不紧张时可以保留它,而在内存紧张时释放它以为更重要的对象让渡内存空间。这时应该怎么做呢?

Java已经考虑到了这种情况,Java的引用中并不是只有“引用”、“不引用”这两种情况,而是有四种情况。

下面给出了强引用、软引用、弱引用的示例。

// 通过等号直接建立的引用都是强引用
User user = new User();

// 通过SoftReference建立的引用是软引用
SoftReference<User> softRefUser =new SoftReference<>(new User());

// 通过WeakReference建立的引用是弱引用
WeakReference<User> weakRefUser = new WeakReference<>(new User());

2 ReferenceQueue

如果一个对象只有软引用或者弱引用,则它随时可能会被JVM垃圾回收掉。于是它就成了薛定谔的猫,在我们读取它之前,根本无法知道它是否还存在。

可是,有时我们需要知道被软引用或者弱引用的对象在何时被回收,以便于进行一些后续的处理工作。ReferenceQueue类便提供了这样的功能。ReferenceQueue本身是一个列表,我们可以在创建软引用或者弱引用的包装对象时传入该列表。这样,当JVM回收被包装的对象时,会将其包装类加入到ReferenceQueue中。

我们可以通过一个可能并不恰当的例子来理解这些概念。假设我们的目标对象是雪糕,软引用或者弱引用的包装对象就是雪糕棒。我们虽然持有了雪糕棒,但是雪糕棒上的雪糕却随时可能融化后掉在地上(也可能是被我们偷吃了,总之是没有了,相当于被JVM销毁了)。ReferenceQueue是我们收集雪糕棒的小木桶,当我们发现某根雪糕棒上的雪糕消失时,就会把雪糕棒放到小木桶中。这样一来,我们只要观察小木桶,就能知道哪些雪糕已经消失了。

我们用示例来展示ReferenceQueue的作用。下面代码给出了目标对象User的源码,它的toString方法会返回包含自身id的字符串。

public class User {
    private long id;

    public User() {
    }

    public User(long id) {
        this.id = id;
    }
    
    @Override
    public String toString() {
        return "User:" + id;
    }
}

下面是主方法的源码。我们对目标对象User建立弱引用包装,在建立包装的构造方法中传入了ReferenceQueue。这样,当被User对象被清理后,它对应的包装对象WeakReference会被放入ReferenceQueue中。

// 创建ReferenceQueue
// 即我们的小木桶
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

// 用来存储弱引用的目标对象
// 即我们用来抓带有雪糕的雪糕棒的手
List<WeakReference> weakRefUserList = new ArrayList<>();
// 创建大量的弱引用对象,交给weakRefUserList引用
// 即创建许多带有雪糕的雪糕棒,并且拿到手里
for (int i =0 ; i< 1000000; i++) { // 创建这么多的目的是为了让内存空间紧张
    // 创建弱引用对象,并在此过程中传入ReferenceQueue
    // 即将雪糕放到雪糕棒上,并且确定用来收集雪糕棒的小木桶
    WeakReference<User> weakReference = new WeakReference(new User(Math.round(Math.random() * 1000)),referenceQueue);
    // 引用弱引用对象
    // 即抓起这个带有雪糕的雪糕棒
    weakRefUserList.add(weakReference);
}

WeakReference weakReference;
Integer count = 0;

// 处理被回收的弱引用
// 即通过检查小木桶,处理没有了雪糕的雪糕棒
while ((weakReference = (WeakReference) referenceQueue.poll()) != null) {
    // 虽然弱引用存在,但是引用的目标对象已经为空
    // 即虽然雪糕棒在木桶中,但是雪糕棒上却没有了雪糕
    System.out.println("JVM 清理了:" + weakReference + ", 从WeakReference中取出对象值为:" + weakReference.get());
    count ++;
}

// 被回收的弱引用总数
// 即小木桶中雪糕棒的数目,也是融化的雪糕的数目
System.out.println("weakReference中的元素数目为:" + count);

// 在弱引用的目标对象不被清理时,可以引用到目标对象
// 即在雪糕还没有融化掉到地上时,雪糕棒上是有雪糕的
System.out.println("在不被清理的情况下,可以从WeakReference中取出对象值为:" +
        new WeakReference(new User(Math.round(Math.random() * 1000)),referenceQueue).get());

运行后可以得到下图所示的运行结果。

图 19-2 程序运行结果

可见,被清理的User对象(相当于雪糕)的包装对象WeakReference(相当于雪糕棒)都被写入了ReferenceQueue(相当于小木桶)中,也正因为它们包装的User对象已经被清理,因此从ReferenceQueue取出的结果必定是null。

ReferenceQueue也可以用在SoftReference中,和在WeakReference中的使用情况类似,我们不再单独介绍。

3 总结

软引用和弱引用是十分有用的。例如,我们在创建缓存时,希望在不影响正常对象的情况下,充分占用内存作为缓存。但当内存空间不够时,又希望释放缓存空间作为它用。这时,使用软引用或者弱引用就再合适不过了。MyBatis就采用上述方式实现了缓存,要想了解其具体实现方式可以参考《通用源码阅读指导书——MyBatis源码详解》一书。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。

可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐