本文简单谈一谈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的好处是, 当内存不足时缓存能够被回收,腾出一些内存给其他更为紧急的用处。
参考
一些思维导图和并发编程学习笔记可参考以下方式领取