0%

JDK源码 Java Reference

之前探讨过一次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
93
public class ClassSoft {

public static class Referred {
/**
* 不是必须实现,和Strong不同。
* 实现该方法是为了追踪GC,
* 实现后也会被当作Finalizer
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}

@Override
public String toString() {
return "I am Referred";
}
}

public static void collect() throws InterruptedException {
System.gc();
Thread.sleep(2000);
}

static class CheckRefQueueThread extends Thread{
@Override
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
*/
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}

@Override
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
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
/**
* Created by childe on 2017/3/31.
*/
public class ClassPhantom {

public static class Referred {
@Override
protected void finalize() throws Throwable {
System.out.println("Referred对象被垃圾收集");
}

@Override
public String toString() {
return "Referredqq";
}
}

public static void collect() throws InterruptedException {
System.gc();
Thread.sleep(2000);
}

static class CheckRefQueueThread extends Thread{
@Override
public void run() {
Reference<Referred> obj = null;
try {
obj = (Reference<Referred>) phantomQueue.remove();
}
catch (InterruptedException e) {
e.printStackTrace();
}
if(obj != null) {
//因为虚引用的指示对象总是不可到达的,所以此方法总是返回 null
System.out.println("Object for phantomReference is " + obj.get());
try {
Field referent = Reference.class.getDeclaredField("referent");
referent.setAccessible(true);
Object result = referent.get(obj);
System.out.println("gc will collect: " + result.getClass() + "@" + result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
};
}

static ReferenceQueue<Referred> phantomQueue = new ReferenceQueue<>();

/**
* -Xms4m -Xmx4m
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("创建一个软引用");

Referred strong = new Referred();
PhantomReference<Referred> soft = new PhantomReference<>(strong, phantomQueue);
new CheckRefQueueThread().start();

collect();

System.out.println("切断强引用");

strong = null;
collect();

System.out.println("开始堆占用");

try {
List<byte[]> bytes = new ArrayList<>();
while (true) {
bytes.add(new byte[1024*1024]);
collect();
}
} catch (OutOfMemoryError e) {
// 软引用对象应该在这个之前被收集
System.out.println("内存溢出...");
}

System.out.println("Done");
}
}

输出如下:

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
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
public abstract class Reference<T> {
//......
//引用有4中概念上的状态:Active、Pending、 Enqueued 、Inactive
//引用的初始态为Active或者Pending,它的生命后期为:(Active || Pending)-> Enqueued -> Inactive
private T referent; /* Treated specially by GC 由GC专门处理*/

ReferenceQueue<? super T> queue; /* Reference 关联的引用队列 */

Reference next; /* 指向下一个引用 */
//......
}

public class ReferenceQueue<T> {
//......
//如果我们构造Reference时,未传入自定义队列,默认使用此队列。
private static class Null extends ReferenceQueue {
//入队操作直接返回
boolean enqueue(Reference r) {
return false;
}
}

static ReferenceQueue NULL = new Null();
static ReferenceQueue ENQUEUED = new Null();

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class 只会对Reference类调用该方法 */
synchronized (r) {
//以入队的引用不多次入队
if (r.queue == ENQUEUED) return false;
synchronized (lock) {
//修改引用入队状态为Enqueued
r.queue = ENQUEUED;
//插入对头
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
//通知等待在锁上的线程ReferenceQueue.remove()
lock.notifyAll();
return true;
}
}
}

private Reference<? extends T> reallyPoll() { /* Must hold lock 必须在持有lock锁的情况下执行,lock由其外层方法获取 */
if (head != null) {
//获取队头
Reference<? extends T> r = head;
head = (r.next == r) ? null : r.next;
//将关联的队列置为NULL,此时r的状态为Inactive,处于此状态的引用不会再发生变化,等待被回收。
r.queue = NULL;
r.next = r;
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
//......
}

扩展WeakHashMap

JDK中有对引用的具体使用,当我们需要实现一个简单的本地内存敏感缓存时,可以考虑使用WeakHashMap,此处不再分析其源码。WeakHashMap的每个Entry都是WeakReference的子类,每次put或者get或者resize扩容时,都会调用WeakHashMap的expungeStaleEntries方法,清除那些被GC加入到ReferenceQueue的Entry。

小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!