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

---来自《深入理解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<byte[]> softReference = new SoftReference<byte[]>(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<byte[]> softReference = new WeakReference<byte[]>(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<ThreadLocal<?>> {
    /** 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<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> 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<User,User> weakHashMap = new WeakHashMap<User, User>();
        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());

    }
}

输出如下:

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<K,V> {

    private final int size;

    private final Map<K,V> eden;

    private final Map<K,V> 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<User> softReference = new SoftReference<User>(null);
        if(softReference.get() != null) {
            System.out.println(softReference.get());
        } else {
            softReference = new SoftReference<User>(service.getUser());
        }
    }
}

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

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

参考

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

03-05 20:15