乐趣区

关于java:谈谈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<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 的益处是, 当内存不足时缓存可能被回收,腾出一些内存给其余更为紧急的用途。

参考

  • java 中的 Reference 类型
  • 软援用、弱援用、虚援用 - 他们的特点及利用场景
  • ThreadLocal 弱援用与内存透露剖析
  • 《深刻了解 java 虚拟机》

一些思维导图和并发编程学习笔记可参考以下形式支付

退出移动版