谈谈java中的引用

封面图

本文简单谈一谈java中的各种引用。

java中传统引用定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

---来自《深入理解java虚拟机》

这个定义就是指的强引用,这种强引用只能描述两种情况,被引用和未被引用,为了能表示内存充足就引用着,内存不足就回收的情况,又搞出来三个(其实是四个,还有一个FinalReference,它与finalize方法有关,深入理解java虚拟机这本书说不推荐用,笔者也没研究,就不谈了)表示不同强弱程度的引用,这个强弱程度与GC有关。

几种引用的介绍

强引用

这种引用我们再熟悉不过了,比如像下边这样

User user = new User();
// 或者
byte[] user = new User[10];

强引用的特点就是:当有强引用存在时,就算将要发生OOM了也不会被回收。当然需要注意的是当gc root不可达时,就算被强引用也是会被回收的。比如下边这样的:

A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null
b = null;

这个例子中虽然a对象被b对象的a属性强引用着,b对象被a对象的b属性强引用着,但是通过可达性分析看,他们是不可达的,所以会在下一次gc时被回收。

下面看一个强引用的测试用例,注意jvm参数中将堆内存控制在10m,并且打印出gc日志

// -Xms20m -Xmx20m -XX:+PrintGC
public class StrongReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];
        System.out.println("gc前:"+ bytes);
        System.gc();
        Thread.sleep(300);
        System.out.println("gc后:"+bytes);
        System.out.println("再分配一个10M模拟堆内存不足,看看之前的bytes会不会被回收");
        byte[] bytes1 = new byte[_10M];
        Thread.sleep(1000);
    }
}

如下是输出结果:

强引用测试结果

调用System.gc()后,发现做了一次Minor GC, 一次Full GC, Minor GC回收了很多内存,Full GC 则没有回收多少内存,gc后,发现还是能找到bytes这个数组(打印出来了内存地址),所以说明他没有被回收(要是这种强引用都被回收就没法玩了)

接下来有分配一个10M的数组,显然内存不够了,从gc日志来看,他尝试做了几次gc,但是因为我们的bytes是强引用,所以没法回收,抛出OOM了。

软引用SoftReference

软引用的特点是当要发生OOM前,他引用的对象或者内存块会在gc时会被回收。

// -Xms20m -Xmx20m -XX:+PrintGC
public class SoftReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];

        SoftReference softReference = new SoftReference(bytes);
        bytes = null; // 这个很关键,把强引用给断开,否则测试会发现一个OOM
        System.out.println("gc前:" + softReference.get());
        Thread.sleep(500);
        System.gc();
        Thread.sleep(500);
        System.out.println("gc后:" + softReference.get());
        Thread.sleep(500);
        System.out.println("再分配一个10M, 模拟堆内存不足");
        byte[] bytes1 = new byte[_10M];
        System.out.println("分配后:" + softReference.get());
    }
}

输出如下

软引用测试结果

非常神奇,也是两个10M,这次没有发生OOM!注意到红框框这一行,发现回收了10292kb,大概是10M,结合后面的分配后:null, 可以看出SoftReference引用的对象在发生OOM前被回收了。

这里还需要注意输出的第2行和第3行,发现虽然发生了gc,但是那个10M的数组没被回收,这里需要与接下来的WeakReference对比看。

弱引用WeakReference

WeakReference的特点是发生下一次gc时回收被引用的对象,不管内存是否充足,这里需要注意对比与SoftReference的区别

接下来还是一个测试用例, 只用将上边例子中的SoftReference改为WeakReference即可:

// -Xms20m -Xmx20m -XX:+PrintGC
public class WeakReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];

        WeakReference softReference = new WeakReference(bytes);
        bytes = null; // 这个很关键,把强引用给断开,否则测试会发现一个OOM
        System.out.println("gc前:" + softReference.get());
        Thread.sleep(500);
        System.gc();
        Thread.sleep(500);
        System.out.println("gc后:" + softReference.get());
        Thread.sleep(500);
        System.out.println("再分配一个10M, 模拟堆内存不足");
        byte[] bytes1 = new byte[_10M];
        System.out.println("分配后:" + softReference.get());
    }
}

输入结果:

弱引用测试结果

注意到第一个10M的数组在第一次gc时就被回收了,但其实这时的内存是充足的。

虚引用PhantomReference

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

---来自《深入理解jvm虚拟机》

这个东西笔者看了很久,发现不得要领,做测试也不太好弄,不知道这种引用到底有啥用。关于使用方法方面,一些博文说是要和ReferenceQueue配合着使用。

如下是一篇看起来不错的文章,有兴趣的读者自行研究吧,笔者不费这个精力了。

《在Java中使用PhantomReference析构资源对象》

使用场景

ThreadLocal中对WeakReference的使用

static class Entry extends WeakReference> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

这个主要是保证当你定义的ThreadLocal不被引用时,里边的ThreadLocalMap能被回收。

参考这篇文章《ThreadLocal与WeakReference》,几句话说得还挺清楚

WeakHashMap中对WeakReference的使用

与之相关的一段源码是下边这个样子的:

private static class Entry extends WeakReference implements Map.Entry {
    V value;
    final int hash;
    Entry next;

    /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue queue,
          int hash, Entry next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
    // 此处略去很多代码,我也没看
} 
 

在没有hash冲突的情况下,WeakHashMap就相当于维护了一个Entry数组,而Entry的key是WeakFeference引用, 所以可以猜想,如果外边没有对某个key的引用,那么下一次gc时,这个key指向的对象就会被回收。

还是做一个实验验证一下:

class User {
    private byte[] bytes = new byte[5 * 1024 *  1024];
}

public class WeakHashMapTest {
    public static void main(String[] args) throws InterruptedException {
        WeakHashMap weakHashMap = new WeakHashMap();
        User u2 = new User();
        weakHashMap.put(u2, new User());
        System.out.println("size1="+weakHashMap.size());
        System.gc(); // 1
        Thread.sleep(500);
        System.out.println("size2="+weakHashMap.size());
        System.out.println("---------");
        u2 = null;
        System.gc(); // 2
        Thread.sleep(500);
        System.out.println("size3="+weakHashMap.size());

    }
}

输出如下:

WeakHashMap测试结果

1处gc后发现内存没有5M的变化,因为key被u2引用着;

将u2值为null, key除了被WeakHashMap弱引用着,没别的引用了,所以调用gc后被回收,内存减少大约5M,size3变为0。5M是key指向的对象占用的内存。

如果再调用一次gc,会发现还会gc掉5M, 这个就是value指向的对象了。

WeakHashMap常被用来做缓存,看到博客里边常有人用tomcat的一个缓存的源码举例,笔者还没看过tomcat源码,这里直接抄一个过来

package org.apache.tomcat.util.collections;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public final class ConcurrentCache {

    private final int size;

    private final Map eden;

    private final Map longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

通过这种方式,将常用的key放到eden强引用里边,不常用的放到longterm里边,longterm是个WeakHashMap, 没有人引用key下一次gc就可以自动回收掉。做得还是挺巧妙的。

SoftReference的应用-本地缓存

public class Cache {

    public static void main(String[] args) {
        Service service = new Service();
        SoftReference softReference = new SoftReference(null);
        if(softReference.get() != null) {
            System.out.println(softReference.get());
        } else {
            softReference = new SoftReference(service.getUser());
        }
    }
}

将拿到的user用弱引用引用着,每次都softReference查,查到则命中缓存,减少对service请求。

用SoftReference的好处是, 当内存不足时缓存能够被回收,腾出一些内存给其他更为紧急的用处。

参考

一些思维导图和并发编程学习笔记可参考以下方式领取

二维码

你可能感兴趣的:(java)