之前探讨过一次JAVA的FinalReference,这次我们来看下java.lang.ref包下对应的其他三种引用。
走近引用
Reference和ReferenceQueue在使用中一定是结伴出现的,当一个Reference确定要被GC回收,GC便会把Reference加入到与之关联的ReferenceQueue中。注意:在Reference的构造方法中,我们可以传入一个注册队列ReferenceQueue,这个队列我们稍后会具体看,需要主要的是,这个队列需要单独的线程去做消费,否则会存在OOM的隐患。
这些引用可用来实现不同的缓存类型(内存敏感和内存不敏感),大名鼎鼎的Guava cache就是基于引用的这些特性来实现高速本地缓存。
StrongReference(强引用)
我们平时开发中new一个对象出来,这种引用便是强引用。 JVM 系统采用 Finalizer 来管理每个强引用对象 , 并将其被标记要清理时加入 ReferenceQueue, 并逐一调用该对象的 finalize() 方法。具体详见我的前一篇博客:JDK源码 FinalReference
SoftReference(软引用)
当内存足够的时候,软引用所指向的对象没有其他强引用指向的话,GC的时候并不会被回收,当且只当内存不够时才会被GC回收(调用finalize方法)。强度仅次于强引用。GC回收前,会将那些已经向引用队列注册的新清除的软引用加入队列。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93public class ClassSoft {
public static class Referred {
/**
* 不是必须实现,和Strong不同。
* 实现该方法是为了追踪GC,
* 实现后也会被当作Finalizer
* @throws Throwable
*/
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}
public String toString() {
return "I am Referred";
}
}
public static void collect() throws InterruptedException {
System.gc();
Thread.sleep(2000);
}
static class CheckRefQueueThread extends Thread{
public void run() {
Reference<Referred> obj = null;
try {
obj = (Reference<Referred>)softQueue.remove();
}
catch (InterruptedException e) {
e.printStackTrace();
}
if(obj != null) {
try {
Field referent = Reference.class.getDeclaredField("referent");
referent.setAccessible(true);
Object result = referent.get(obj);
//此处异常可以说明,在被放入队列之前referent已经被JVM置为null
System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Object for SoftReference is " + obj.get());
}
}
}
//如果我们使用了自定义的注册队列,一定要启动一个线程来处理该队列
//JVM只负责像队列中放入对象,不负责清理
static ReferenceQueue<Referred> softQueue = new ReferenceQueue<>();
/**
* JVM配置
* -Xms4m -Xmx4m
* -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
* 务必加上该参数,以确定collect方法后GC被执行
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建软引用");
Referred strong = new Referred();
SoftReference<Referred> soft = new SoftReference<>(strong,softQueue);
new CheckRefQueueThread().start();
ClassSoft.collect();
System.out.println("切断强引用");
strong = null;
ClassSoft.collect();
System.out.println("GC之前,软引用值:" + soft.get().toString());
System.out.println("开始堆占用");
try {
List<byte[]> bytes = new ArrayList<>();
while (true) {
bytes.add(new byte[1024*1024]);
ClassSoft.collect();
}
} catch (OutOfMemoryError e) {
// 软引用对象应该在这个之前被收集
System.out.println("内存溢出...");
}
System.out.println("Done");
}
}
程序输出如下:1
2
3
4
5
6
7
8
9
10创建软引用
切断强引用
GC之前,软引用值:I am Referred
开始堆占用
java.lang.NullPointerException
Referred对象被垃圾收集
at com.cxd.jvm.references.ref.ClassSoft$CheckRefQueueThread.run(ClassSoft.java:54)
Object for SoftReference is null
内存溢出...
Done
我们可以看到,软引用在GC回收前,调用get方法是可以返回其关联的实际对象的,当其被GC加入ReferenceQueue前,JVM会将其关联的对象置为null。
WeakReference(弱引用)
弱引用指向的对象没有任何强引用指向的话,GC的时候会进行回收。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53/**
*
* Created by childe on 2017/3/31.
*/
public class ClassWeak {
public static class Referred {
/**
* 不是必须实现,和Strong不同。
* 实现该方法是为了追踪GC
* 实现后也会被当作Finalizer
* @throws Throwable
*/
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}
public String toString() {
return "I am weak";
}
}
public static void collect() throws InterruptedException {
System.gc();
Thread.sleep(2000);
}
/**
* JVM配置
* -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
* 务必加上该参数,以确定collect方法后GC被执行
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个弱引用");
Referred strong = new Referred();
WeakReference<Referred> weak = new WeakReference<>(strong);
ClassWeak.collect();
System.out.println("切断强引用");
strong = null;
System.out.println("GC之前,弱引用值:" + weak.get().toString());
ClassWeak.collect();
System.out.println("Done");
}
}
程序输出如下:1
2
3
4
5创建一个弱引用
切断强引用
GC之前,弱引用值:I am weak
Referred对象被垃圾收集
Done
PhantomReference(虚引用)
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。这个特性,决定了他的get方法每次调用均会返回null。
1 | /** |
输出如下:1
2
3
4
5
6
7
8创建一个软引用
切断强引用
Referred对象被垃圾收集
开始堆占用
Object for phantomReference is null
gc will collect: class com.cxd.jvm.references.ref.ClassPhantom$Referred@Referredqq
内存溢出...
Done
引用间的差别
我们注意到虚引用在被加入到ReferenceQueue中后,关联对象并没有被置为null,这点和弱引用及软引用不同。这也是我开头说的潜在OOM的最大风险。当然,这种现象只是加速了OOM问题的暴露,并不是根本原因。JVM GC的这个模型可以看作是生产-消费模型,GC是生产者,我们自己起的线程是消费者(Finalizer中JDK自带线程),当只有生产者时,OOM是迟早的事情。
ReferenceQueue
我们介绍的这四种引用都从java.lang.ref.Reference继承,Reference是个单向链表,ReferenceQueue利用Reference的这个特性来维护先进后出单向队列(类似栈)。
1 | public abstract class Reference<T> { |
扩展WeakHashMap
JDK中有对引用的具体使用,当我们需要实现一个简单的本地内存敏感缓存时,可以考虑使用WeakHashMap,此处不再分析其源码。WeakHashMap的每个Entry都是WeakReference的子类,每次put或者get或者resize扩容时,都会调用WeakHashMap的expungeStaleEntries方法,清除那些被GC加入到ReferenceQueue的Entry。
小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!