java引用类型

158 阅读12分钟

引用强弱关系:强引用 > 软引用 > 弱引用 > 虚引用

强引用

强引用在代码中普遍存在的引用,形如:Object obj = new Object()的引用。垃圾回收器永远不会回收强引用对象,因此当进程中持有过多强引用对象导致内存不足时,虚拟机便会抛出OutofMemoryException异常。

弱引用

弱引用时一种非必需的对象引用,在垃圾回收时,如果对象只存在弱引用,则垃圾回收器会回收该对象,因此只有弱引用的对象只能生存到下一次垃圾回收之前,jdk提供WeakReference创建弱引用。

弱引用通常用来避免内存泄漏的问题,比如在android开发中通常通过弱引用的形式持有Activity实例,避免造成Activity泄漏的问题。

下面通过两个demo感受一下弱引用的特性,先是对象还存在强引用的情况:

StrongInstance instance = new StrongInstance();
WeakReference<StrongInstance> weakRef = new WeakReference<>(instance);
System.gc();
​
StrongInstance getInstance = weakRef.get();
System.out.println("weakRef get: " + getInstance);
Assert.assertNotNull(getInstance);

其输出如下:

weakRef get: com.example.refrencetest.RefTest$StrongInstance@69c81773

由于当前还存在StrongInstance的强引用instance,因此在System.gc()触发垃圾回收之后,weakRef仍可正常获取到StrongInstance的引用,因此输出不会是null,如果对象只有弱引用存在,则在

如果对象只有弱引用存在,则在下一次垃圾回收时就会将该对象回收,如下:

StrongInstance instance = new StrongInstance();
WeakReference<StrongInstance> weakRef = new WeakReference<>(instance);
instance = null; // ---- 1 ----
System.gc();
​
StrongInstance getInstance = weakRef.get();
System.out.println("weakRef get: " + getInstance);
Assert.assertNull(getInstance);

其输出如下:

weakRef get: null

由于在注释1处主动清空StrongInstance的强引用对象,因此因此在System.gc()触发垃圾回收之后,weakRef所引用的实例被垃圾回收器所回收,因此输出为null。

ReferenceQueue

引用队列主要作用是观测所引用的对象是否被垃圾回收器回收。将Reference对象与引用队列关联后,在其所引用的实例被回收之后,会将Reference对象加到ReferenceQueue中,因此可以通过检测ReferenceQueue中的元素来判断Reference对象所引用的实例是否被回收。

java中各类引用都是Refrence的子类,Reference有一个接收ReferenceQueue对象的构造器,如下:

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = queue;
}

因此将Reference对象与ReferenceQueue相关联的方式就是将ReferenceQueue传入引用对象的构造器中。

ReferenceQueue常用的方法: public Reference<? extends T> poll():从队列中取出一个元素,队列为空则返回null; public Reference<? extends T> remove():从队列中出队一个元素,若没有则阻塞至有可出队元素; public Reference<? extends T> remove(long timeout):从队列中出队一个元素,若没有则阻塞至有可出对元素或阻塞至超过timeout毫秒;

下面是一个ReferenceQueue的使用示例:

ReferenceQueue<Instance> refQueue = new ReferenceQueue<>();
WeakReference<Instance> weakRef = new WeakReference<>(new Instance(), refQueue); // 1
System.out.println("weakRef = " + weakRef); // 2
​
System.gc(); // 3
​
System.out.println("after gc instance ref = " + weakRef.get()); // 4
System.out.println("refQueue poll " + refQueue.poll()); // 5

其输出如下:

weakRef = java.lang.ref.WeakReference@69c81773
after gc instance ref = null
refQueue poll java.lang.ref.WeakReference@69c81773

在注释1处构造WeakReference对象时传入构造好ReferenceQueue对象,在触发gc之后会将weakRef加入到refQueue之中,通过refQueue.poll()能获取到被销毁实例的Reference对象,从注释5处的输出可以看出,refQueue.poll()获取到的WeakReference对象确实是之前创建的weakRef

软引用

软引用是描述哪些还有用,但并非必需的对象,它比弱引用稍强一些。当对象只被软引用关联时,只有在内存不够,即将发生OOM异常之前才会会回收被软引用关联的对象,如果回收完后还是没有足够的内存,就会抛出OutofMemoryException.

在jvm平台上,软引用适用于内存敏感的缓存,在android上却不建议使用软引用作为缓存。这是因为软引用作为缓存时缺乏足够的信息判断当前对象是否应该被清除,如果过早就把缓存清除,这导致缓存效果大打折扣,如果清除太晚了又浪费内存,因此在android平台上建议使用LruCache做缓存。

下面实际体验一下软引用,在jvm平台可以通过指定Xmx参数控制jvn的最大heap值,在运行程序时加上-Xmx20M指定最大heap为20M:

public void testSoftRef() {
    SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024 * 10]);
    System.out.println("softRef: before gc " + softRef.get());
    System.gc();
    System.out.println("softRef: after gc" + softRef.get()); // 1
​
    byte[] bytes = new byte[1024 * 1024 * 10];
    System.gc();
    System.out.println("softRef: after alloc new bytes " + softRef.get()); // 2
}

其输出如下:

softRef: before gc [B@330bedb4
softRef: after gc[B@330bedb4
softRef: after alloc new bytes null

程序中先申请10M空间,此时由于还有充足的内存空间,因此第一次gc后对象未被回收,此时注释1处能正常获取到对象引用。继续申请10M空间,再触发gc,此时内存告急,会将softRef关联的byte数组回收,因此注释2处无法获取对象引用,即输出null。

在android上运行以下代码:

@Test
public void testSoftRef1() {
    ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    Map<String, SoftReference<byte[]>> cache = new HashMap<>();
    int i = 0;
    for (; i < 200; i++) {
        cache.put(String.valueOf(i), new SoftReference<>(new byte[1024 * 1024], queue));
    }
    Log.i(TAG, "testSoftRef1: alloc 1Mb object until key " + i);
    boolean cacheAdded = false;
    while (Runtime.getRuntime().freeMemory() > 1024) {
        cacheAdded = true;
        cache.put(String.valueOf(i), new SoftReference<>(new byte[1024], queue));
        i++;
    }
    if (cacheAdded) {
        Log.i(TAG, "testSoftRef1: alloc 1kb object until key " + i);
    }
    cacheAdded = false;
    while (Runtime.getRuntime().freeMemory() > 1) {
        cacheAdded = true;
        cache.put(String.valueOf(i), new SoftReference<>(new byte[1], queue));
        i++;
    }
    if (cacheAdded) {
        Log.i(TAG, "testSoftRef1: alloc 1byte object until key " + i);
    }
    
    System.gc(); // 1
​
    int gcSize = 0;
    Reference<?> bytesRef = null;
    while ((bytesRef = queue.poll()) != null) {
        gcSize++;
    }
    Log.i(TAG, "testSoftRef1: gcSize " + gcSize); // 2
    Assert.assertTrue(gcSize > 0);
    Assert.assertEquals(gcSize, destroyedRef);
}

输出如下:

08-18 09:34:57.842 I/TestRunner(18230): started: testSoftRef1(com.example.refrencetest.RefTest)
08-18 09:34:57.860 I/RefTest (18230): testSoftRef1: alloc 1Mb object until key 200
08-18 09:34:57.865 I/RefTest (18230): testSoftRef1: gcSize 188
08-18 09:34:57.865 I/TestRunner(18230): finished: testSoftRef1(com.example.refrencetest.RefTest)

通过日志可以知道,在gc(注释1)之前申请了两百个SoftReference作为缓存,占用200M内存,gc后释放了188个(注释2)缓存的SoftReference对象,即释放188M内存,说明一次gc就让大部分缓存失效,这样的缓存效率有点堪忧啊。

虚引用

虚引用是最弱的引用关系,也称幽灵引用或幻影引用,虚引用对于对象的生命周期没有任何影响,给对象设置虚引用的目的常常在于监控对象是否真正被回收。

虚引用只有接收ReferenceQueue的构造函数,这表示它只能和ReferenceQueue结合使用。

public PhantomReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}

虚引用的作用

给对象设置虚引用的目的常常在于监控对象是否真正被回收。

首先要了解:重写类的finalize()方法会对象至少要经历两次gc周期。

软引用(弱引用)的缺陷

不是通过ReferenceQueue就能检测到关联对象是否被回收吗?为什么又要用虚引用来检测呢?为了讲清这个问题,先看下面的demo:

static class Instance2 {
    static Instance2 instance;
    private byte[] data = new byte[1024];
​
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Instance1 finalize");
        instance = this;
    }
}
​
@Test
public void testWeakRefQueue() throws InterruptedException {
    ReferenceQueue<Instance2> refQueue = new ReferenceQueue<>();
    WeakReference<Instance2> weakRef = new WeakReference<>(new Instance2(), refQueue);
    System.out.println("weakRef = " + weakRef);
​
    System.gc();
​
    System.out.println("after gc instance ref = " + weakRef.get());
    System.out.println("refQueue remove " + refQueue.remove(100));
}

testWeakRefQueue测试用例中,通过WeakReference持有Instance2的实例,类Instance2重写了finalize方法,并在finalize中把自己复活了,即把自己重新赋值给静态变量instance,使自己从从垃圾回收中被豁免,即当前实例并没有真正被回收,其输出如何呢?

weakRef = java.lang.ref.WeakReference@69c81773
after gc instance ref = null
Instance1 finalize
refQueue remove java.lang.ref.WeakReference@69c81773

如上所示,显示输出了finalize方法中的Instance1 finalize,接着成功从refQueue中获取到了“被回收”的WeakReference对象。

软引用也是如此,有兴趣者可以自行尝试。

这说明:在弱引用(或软引用)WeakReference中,gc时,将引用对象加入ReferenceQueue中是先于finalize调用的(或与finalize调用同时发生)

也就是说,如果在finalize中进行自救,那么ReferenceQueue并不能察觉,因此在软引用和弱引用中是不能准确的检测出对象何时才真正被回收的。

虚引用的不同

再来看看虚引用在这种情况下是如何工作的:

static class PhantomInstance {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("PhaontomInstance finalize start");
        super.finalize();
        System.out.println("PhaontomInstance finalize end");
    }
}
​
@Test
public void testPhantomRef() throws InterruptedException {
    ReferenceQueue<PhantomInstance> refQueue = new ReferenceQueue<>();
    PhantomInstance instance = new PhantomInstance();
    PhantomReference<PhantomInstance> phantomRef = new PhantomReference<>(instance, refQueue);
    instance = null;
​
    int gcCount = 0;
    while (true) {
        System.gc();
        gcCount++;
        System.out.println("phantomRef: gc");
        if (refQueue.remove(100) != null) {
            System.out.println("phantomRef: released");
            break;
        }
    }
    Assert.assertEquals(2, gcCount);
}

PhantomInstance同样重写了finalize方法,在testPhantomRef中创建PhantomReference之后,通过循环触发gc来观测PhantomReference关联对象的回收情况,这里预期是执行两次gc之后被正常回收,其输出如下:

phantomRef: gc
PhaontomInstance finalize start
PhaontomInstance finalize end
phantomRef: gc
phantomRef: released

从这个用例不难看出在PhantomInstance实例的finalize方法被调用之前,refQueue中并无相关的引用对象,直到finilize执行完成后,才能从refQueue中取出引用对象。

那如果在finalize方法中进行自救,会发生什么呢:

static class PhantomInstanceLeak {
    static PhantomInstanceLeak instance;
​
    @Override
    protected void finalize() throws Throwable {
        System.out.println("PhantomInstanceLeak finalize start");
        super.finalize();
        System.out.println("PhantomInstanceLeak finalize end");
        instance = this; // rescue self from GC
    }
}
​
@Test
public void testPhantomRefWithLeak() throws InterruptedException {
    ReferenceQueue<PhantomInstanceLeak> refQueue = new ReferenceQueue<>();
    PhantomInstanceLeak instance = new PhantomInstanceLeak();
    PhantomReference<PhantomInstanceLeak> phantomRef = new PhantomReference<>(instance, refQueue);
    instance = null;
​
    int gcCount = 0;
​
    out:
    while (true) {
        System.gc();
        gcCount++;
        System.out.println("testPhantomRefWithLeak: gc");
        if (refQueue.remove(100) != null) {
            System.out.println("testPhantomRefWithLeak: released");
            break;
        } else if (PhantomInstanceLeak.instance != null) {
            System.out.println("testPhantomRefWithLeak: recovery from gc, clear it");
            PhantomInstanceLeak.instance = null;
        }
    }
​
    Assert.assertEquals(2, gcCount);
}

PhantomInstanceLeakfinalize方法中通过静态变量instance实例进行自救,仍旧在在testPhantomRefWithLeak中循环gc,直到

refQueue中取出被回收的引用对象,不同的是,如果检测到PhantomInstanceLeak.instance不为空,说明此时PhantomInstanceLeak自救成功,需将其置空,以便继续回收流程。

其输出如下:

testPhantomRefWithLeak: gc
PhantomInstanceLeak finalize start
PhantomInstanceLeak finalize end
testPhantomRefWithLeak: recovery from gc, clear it
testPhantomRefWithLeak: gc
testPhantomRefWithLeak: released

此时发生了两次gc,第一次gc触发finallize方法,其自救成功,随后一次gc才真正回收该对象。

在来看看没有重写finalize的虚引用是怎么工作的:

static class Instance {
    private byte[] data = new byte[1024];
}
​
@Test
public void testPhantomRefWithoutFinalize() {
    ReferenceQueue<Instance> refQueue = new ReferenceQueue<>();
    Instance instance = new Instance();
    PhantomReference<Instance> phantomRef = new PhantomReference<>(instance, refQueue);
    instance = null;
​
    int gcCount = 0;
​
    out:
    while (true) {
        System.gc();
        gcCount++;
        System.out.println("phantomRef: gc");
        while (refQueue.poll() != null) {
            System.out.println("phantomRef: released");
            break out;
        }
    }
​
    Assert.assertEquals(1, gcCount);
}

输出结果如下:

phantomRef: gc
phantomRef: released

从用例中可以看出,预计只需要一次gc即可将虚引用关联对象回收,输出结果页证明了确实如此。

这说明虚引用是先调用finalize,并确认对象是真正的不可达对象时,才将其加入到引用队列中。如果对象从finalize中泄漏出去,则不会加入到引用队列中,这说明从虚引用的引用队列中取出的Reference一定是不可达对象的虚引用,它一定会被回收。因此使用常常使用虚引用来监控对象何时被回收。

总的来说,弱引用(软引用)与虚引用的区别在于:

  • 在弱引用(或软引用)WeakReference中,gc时,将引用对象加入ReferenceQueue中是先于finalize调用的(或与finalize调用同时发生)
  • 虚引用是先调用finalize,并确认对象是真正的不可达对象时,才将其加入到引用队列中。如果对象从finalize中泄漏出去,则不会加入到引用队列中

PhantomReference应用实例

利用这个特性,我们可以在收到对象销毁通知时手动释放资源,从而实现为程序实现兜底的释放策略。

考虑一个业务场景,我们每创建一个User对象对应在数据库中生成一条数据,当对象销毁时删除这条数据,

我们先创建相关类:

/**
 * 用户类
 */
static class User {
    public DatabaseClient databaseClient;
    public User() {
        // 初始化客户端
        databaseClient = new DatabaseClient();
        // 创建时数据库创建数据
        this.databaseClient.create();
    }
}
/**
 * 数据库客户端
 */
static class DatabaseClient {
    /**
     * 创建用户数据
     */
    public void create() {
        System.out.println("--数据库创建用户数据--");
    }
    /**
     * 删除用户数据
     */
    public void remove() {
        System.out.println("--数据库删除用户数据--");
    }
}
​
static class UserPhantomReference extends PhantomReference<User> {
    // 保存user的databaseClient 因为取不到user对象
    public DatabaseClient databaseClient;
​
    public UserPhantomReference(User referent, ReferenceQueue<? super User> q) {
        super(referent, q);
        this.databaseClient = referent.databaseClient;
    }
}

先不用PhantomReference:

@Test
public void testUserCase() {
    // 场景:我们的每创建一个User对象对应在数据库中生成一条数据,当对象销毁时删除这条数据
    User obj = new User();
    // 释放这个内存空间
    obj = null;
    // 调用gc
    System.gc();
}

输出如下:

--数据库创建用户数据--

使用PhantomReference监听:

@Test
public void testUserWithPhantomRef() {
    // 场景:我们的每创建一个User对象对应在数据库中生成一条数据,当对象销毁时删除这条数据
​
    // 新建一个对象,开辟一个内存空间
    User obj = new User();
    // 存储被回收的对象
    ReferenceQueue<User> QUEUE = new ReferenceQueue<>();
    // phantomReference使用虚引用指向这个内存空间
    UserPhantomReference phantomReference = new UserPhantomReference(obj, QUEUE);
    // 释放这个内存空间,此时只有phantomReference通过虚引用指向它
    obj = null;
    // 调用gc
    System.gc();
    // 被清除的队列中取出被回收的对象,一般新开一个线程来监控
    while (true) {
        Reference<? extends User> poll = QUEUE.poll();
        if (poll!=null) {
            UserPhantomReference userPhantomReference = (UserPhantomReference) poll;
            // 对象被回收,删除对应数据
            userPhantomReference.databaseClient.remove();
            System.out.println("--obj is recovery--");
            break;
        }
    }
}

输出如下:

--数据库创建用户数据--
--数据库删除用户数据--
--obj is recovery--

可以看出确实满足了需求。

PhantomReference与finalize

前面的场景使用finalize方法也能实现,因为在finalize方法也能添加自己的兜底方案,为什么不用finalize呢?

有几点原因:

  • finalize方法执行的线程是不可控的
  • finalize方法的执行是串行执行的,使用虚引用我们可以并行多线程执行
  • finalize降低了gc效率(导致对象需要多次gc才能被回收),而虚引用不影响(实际上finalize执行时对象还没销毁)

因此一般使用虚引用进行资源的兜底释放,当然了,最好还用完资源后手动释放,避免在程序运行中发生其他问题。

参考资料