介绍
在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable
)状态,程序才能使用它。
从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。
这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue
)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列(ReferenceQueue
)中。软引用是在垃圾回收之后,才加入队列。
弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象,不管当前内存空间足够与否都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue
)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列(ReferenceQueue
)中。弱引用是在垃圾回收之后,才加入队列。
虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue
)联合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
在任何时候,我们都可以调用ReferenceQueue#poll()
来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference
对象。利用这个方法,我们可以检查哪个SoftReference
所软引用的对象已经被回收。
垃圾回收
在垃圾回收时,软引用和弱引用差别不大,JVM都是先把SoftReference
和WeakReference#referent
字段值设置成null,referent
可以认为对象销毁的标志,也就是referent=null
时,之后加入到引用队列(我们可以认为JVM先回收堆对象占用的内存,然后才将软引用或弱引用加入到引用队列。这里回收是在对象不会再生的情况下,因为对象可能在finalize()
中再生);而虚引用则不同,如果某个堆中的对象,只有虚引用,那么JVM会将PhantomReference
加入到引用队列中,JVM不会自动将referent
字段值设置成null(先保留堆对象的内存空间)。referent
是java.lang.ref.Reference
类的私有字段,虽然没有暴露出共有API来访问这个字段,但是我们可以通过反射拿到这个字段的值,这样就能知道引用被加入到引用队列的时候,referent
到底是不是null。SoftReference
和WeakReference
是一样的,这里我们以WeakReference
为例。
例子1
1 | import java.lang.ref.Reference; |
控制台1
2
3
4class java.lang.String@96354
weak=java.lang.ref.WeakReference@54624a40
java.lang.NullPointerException
at TestWeakReference$1.run(TestWeakReference.java:25)
结论:当我们清除强引用,触发GC的时候,JVM检测到new String("abc")
这个堆中的对象只有WeakReference
,那么JVM会释放堆对象的内存,并自动将WeakReference#referent
字段设置成null,所以result.getClass()
会报空指针异常。
换成PhantomReference
控制台打印1
2
3class java.lang.String@96354
weak=java.lang.ref.PhantomReference@54624a40
gc will collect:class java.lang.String@96354
结论:当PhantomReference
加入到引用队列的时候,referent
字段的值并不是null,而且堆对象占用的内存空间仍然存在。也就是说对于虚引用,JVM是先将其加入引用队列(然后才删除对象),当我们从引用队列删除PhantomReference
对象之后(此时堆中的对象是unreachable
的),那么JVM才会释放堆对象占用的内存空间。由此可见,使用虚引用有潜在的内存泄露风险,因为JVM不会自动帮助我们释放,我们必须要保证它指向的堆对象是不可达的。(这里有个问题,如果sleep(3000)
,GC并没有回收它,因为是回收之前加入队列,如果没有回收它,自然会存在对象)所有的 PhantomReference
对象都必须用经过关联的ReferenceQueue
来创建。
例子2
验证软引用,弱引用,虚引用进入队列是不是需要执行finalize()
1
2
3
4
5
6
7
8
9
10
11package demo6;
import java.util.Date;
class B {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize at " + new Date());
}
}
1 | package demo6; |
结果:WeakReference
不是必须进入finalize()
才能进入队列。PhantomReference
则必须进入finalize()
才能进入队列。但这并不能说明对象销毁和队列的顺序。
综合例子1和例子2
WeakReference1
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
44package demo1;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Date;
public class Tem {
public static void main(String[] args) throws Exception {
B b=new B();
ReferenceQueue queue = new ReferenceQueue();
//SoftReference ref1 = new SoftReference(new B(), queue);
WeakReference ref2 = new WeakReference(b, queue);
//PhantomReference ref3 = new PhantomReference(b, queue);
b=null;
while (true) {
Object obj = queue.poll();
if (obj != null) {
System.out.println("queue.poll at " + new Date() + " " + obj);
try {
Field rereferent = Reference.class.getDeclaredField("referent");
rereferent.setAccessible(true);
Object result = rereferent.get(obj);
//说明result是null
System.out.println("gc will collect:" + result.getClass() + "@" + result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
break;
}
System.gc();
System.out.println("run once.");
}
Thread.sleep(100000);
}
}
弱引用,finalize()
和引用队列不是一个线程,没有必须进入finalize()
,才进入引用队列的条件。进入引用队列前,referent
已经是null。1
2
3
4
5run once.
finalize at Mon Sep 26 17:10:13 CST 2016
queue.poll at Mon Sep 26 17:10:13 CST 2016 java.lang.ref.WeakReference@8c9dee3
java.lang.NullPointerException
at demo1.Tem.main(Tem.java:33)
PhantomReference
虚引用,必须进入finalize()
才能进入队列,进入队列referent
还是有值存在。(回收对象之前,加入队列)1
2
3
4
5
6
7run once.
run once.
run once.
finalize at Mon Sep 26 17:33:09 CST 2016
run once.
queue.poll at Mon Sep 26 17:33:11 CST 2016 java.lang.ref.PhantomReference@4ec8c719
gc will collect:class demo1.B@1591599764
对象再生和引用队列问题
1 | package demo2; |
WeakReference1
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
40package demo2;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
*
* 弱引用对象再生,依然进入引用队列
*
*/
public class M {
public static void main(String[] args) throws InterruptedException {
A a=new A();
System.out.println(a.hashCode());
ReferenceQueue queue = new ReferenceQueue();
WeakReference ref = new WeakReference(a, queue);
System.out.println(ref.get());
a=null;
Object obj = null;
obj = queue.poll();
System.out.println("1="+obj);
System.gc();
Thread.sleep(10000);
System.gc();
System.out.println("2="+ref.get());
obj = queue.poll();
//弱引用队列有值,说明对象已经被销毁
//但是A对象又在finalize里再生
//所以不能用队列来控制软引用,弱引用对象的生命周期
//执行GC的时候,A对象状态变成finalized,进入队列,至于对象再生,则状态不能再改变
//因为,进入队列和finalize()是2个不同的线程。可能先进入队列之后,才进入finalize()
System.out.println("3="+obj);
System.out.println(A.a.hashCode());
}
}
1 | 366712642 |
弱引用队列有值,说明对象已经被销毁,但是A对象又在finalize
里再生,所以不能用队列来控制软引用,弱引用对象的生命周期,执行GC的时候,A对象状态变成finalized
,进入队列,至于对象再生,则状态不能再改变。因为进入队列和finalize()
是2个不同的线程。可能先进入队列之后,才进入finalize()
,对象的状态来不及改变。1
2
3
4
5
61022736404
demo2.A@3cf5b814
1=null
2=null
3=java.lang.ref.WeakReference@773de2bd
1022736404
PhantomReference1
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
49package demo2;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
*
* 虚引用再生问题,finalize和进入队列顺序问题
*
*/
public class W {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(new A(), queue);
System.out.println(ref.get());
Object obj = null;
obj = queue.poll();
System.out.println("1=" + obj);
// 第一次gc
System.gc();
Thread.sleep(10000);
System.gc();
System.out.println("2="+ref.get());
obj = queue.poll();
System.out.println("3="+obj); //第1次gc,对象再生,没有加入队列。
//虚引用是必须进入finalize方法才能进入队列,如果对象在此处再生,就不会进入队列。
A.a = null;
// 第二次gc
System.gc();
Thread.sleep(10000);
obj = queue.poll(); //第2次gc,再生对象=null,加入队列。
System.out.println(obj);
}
}
//虚引用更容易控制对象生命周期
/*null
1=null
2=null
3=null
java.lang.ref.PhantomReference@1e638ee4
*/
虚引用是必须进入finalize()
才能进入队列,如果对象在此处再生,就不会进入队列,所以队列里为null。1
2
3
4
5null
1=null
2=null
3=null
java.lang.ref.PhantomReference@1e638ee4
示例
示例1
图画的有点问题,str1
是指向字符串,并不是指向weak1
。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
32import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class ReferenceDemo {
public static void main(String[] args) {
// 引用队列
ReferenceQueue<String> referenceQueue = new ReferenceQueue<String>();
String str = new String("字符串");
// 弱引用
WeakReference<String> weakReference1 = new WeakReference<String>(str, referenceQueue);
WeakReference<String> weakReference2 = new WeakReference<String>(str, referenceQueue);
// 强引用str没有指向字符串
str = null;
// 弱引用指向的字符串,此时指向str1;str1是个强引用
// 如果这里没有强引用,弱引用就会在Gc执行之后被消除
String str1 = weakReference1.get();
// System.out.println("弱引用:" + str1);
System.out.println("引用队列里的引用:" + referenceQueue.poll());
// 执行GC回收
System.out.println("------------------------------------GC回收----------------------------------------------");
for (int i = 0; i < 100; i++) {
System.gc();
}
// 此时弱引用没有被消除,因为str1是强引用
str1 = weakReference1.get();
System.out.println("弱引用1:" + weakReference1.get());
System.out.println("弱引用2:" + weakReference2.get());
System.out.println(str1);
// 引用队列添加被回收的对象
System.out.println("引用队列:" + referenceQueue.poll());
}
}
1 | 引用队列里的引用:null |
结论: 但是str=null,str1=weakReference1.get()
;str1
指向字符串,并且weak1
和weak2
同时有字符串的引用,“字符串”没有销毁,所以weak1
,weak2
都是强引用。
示例2
创建一个String
对象、ReferenceQueue
对象和WeakReference
对象。1
2
3
4
5
6// 创建一个强引用
String str = new String("hello");
// 创建引用队列;表明队列中存放String对象的引用
ReferenceQueue rq = new ReferenceQueue();
// 创建一个弱引用,它引用"hello"对象,并且与rq引用队列关联
WeakReference wf = new WeakReference(str, rq);
在图中,带实线的箭头表示强引用,带虚线的箭头表示弱引用。从图中可以看出,此时”hello”对象被str
强引用,并且被一个WeakReference
对象弱引用,因此”hello”对象不会被垃圾回收。
示例3
把引用”hello”对象的str
变量置为null,然后再通过WeakReference
弱引用的get()
获得”hello”对象的引用。1
2
3
4
5
6
7String str = new String("hello"); //1
ReferenceQueue rq = new ReferenceQueue(); //2
WeakReference wf = new WeakReference(str, rq); //3
str=null; //4 取消"hello"对象的强引用
String str1=wf.get().toString(); //5 假如"hello"对象没有被回收,str1引用"hello"对象
//假如"hello"对象没有被回收,rq.poll()返回null
Reference ref=rq.poll(); //6
“hello”对象只具有弱引用
执行完以上标记4后,内存中引用与对象的关系如图所示,此时”hello”对象仅仅具有弱引用,因此它有可能被垃圾回收。假如它还没有被垃圾回收,那么接下来在标记5执行wf.get()
会返回”hello”对象的引用,并且使得这个对象被str1
强引用。再接下来在标记6执行rq.poll()
会返回null,因为此时引用队列中没有任何引用。ReferenceQueue#poll()
用于返回队列中的引用,如果没有则返回null。
示例4
1 | String str = new String("hello"); //1 |
“hello”对象被垃圾回收,弱引用被加入到引用队列
在以下程序代码中,执行完标记4后,”hello”对象仅仅具有弱引用。接下来两次调用System.gc()
,催促垃圾回收器工作,从而提高 “hello”对象被回收的可能性。假如”hello”对象被回收,那么WeakReference
对象的引用被加入到ReferenceQueue
中, 接下来wf.get()
返回null,并且rq.poll()
返回WeakReference
对象的引用。图显示执行完标记8后内存中引用与对象的关系。
示例5
依次创建10个软引用、10个弱引用和10个虚引用,它们各自引用一个Grocery对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package all;
class Grocery {
private static final int SIZE = 10000;
// 属性d使得每个Grocery对象占用较多内存,有80K左右
private double[] d = new double[SIZE];
private String id;
public Grocery(String id) {
this.id = id;
}
public String toString() {
return id;
}
public void finalize() {
System.out.println("Finalizing " + id);
}
}
1 | package all; |
控制台打印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--------------------创建10个Grocery对象以及10个软引用-------------------------------
Just created soft: soft-0
Just created soft: soft-1
Just created soft: soft-2
Just created soft: soft-3
Just created soft: soft-4
Just created soft: soft-5
Just created soft: soft-6
Just created soft: soft-7
Just created soft: soft-8
Just created soft: soft-9
--------------------创建10个Grocery对象以及10个弱引用-------------------------------
Just created weak: weak-0
Just created weak: weak-1
Just created weak: weak-2
Just created weak: weak-3
Just created weak: weak-4
Just created weak: weak-5
Just created weak: weak-6
Just created weak: weak-7
Just created weak: weak-8
Just created weak: weak-9
--------------------创建10个Grocery对象以及10个虚引用-------------------------------
Finalizing weak-3
Finalizing weak-2
Finalizing weak-1
Finalizing weak-0
Finalizing weak-5
Finalizing weak-4
Just created Phantom: null
Finalizing weak-9
Finalizing weak-8
Just created Phantom: null
Finalizing weak-7
Just created Phantom: null
Finalizing weak-6
Just created Phantom: null
Just created Phantom: null
Just created Phantom: null
Just created Phantom: null
Just created Phantom: null
Just created Phantom: null
Just created Phantom: null
In queue: java.lang.ref.WeakReference@13883d5f : null
Finalizing Phantom-4
Finalizing Phantom-6
Finalizing Phantom-7
Finalizing Phantom-8
Finalizing Phantom-3
Finalizing Phantom-5
Finalizing Phantom-9
Finalizing Phantom-0
Finalizing Phantom-1
Finalizing Phantom-2
结论:虚引用形同虚设,它所引用的对象随时可能被垃圾回收;具有弱引用的对象拥有稍微长的生命周期,当垃圾回收器执行回收操作时,有可能被垃圾回收;具有软引用的对象拥有较长的生命周期,但在Java虚拟机认为内存不足的情况下,也会被垃圾回收。
软引用
1 | MyObject aRef=new MyObject(); |
对于这个MyObject
对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aReference
的强引用,所以这个MyObject
对象是强可及对象。
Java虚拟机的垃圾收集线程对软可及对象和其他一般Java对象进行区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。
也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError
之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。SoftReference
的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
也就是说, 一旦SoftReference
保存对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference
类所提供的get()
返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()
将返回null。
软引用实现缓存
1 | package soft; |
1 | package soft; |
弱引用
在Java集合中有一种特殊的Map
类型WeakHashMap
, 在这种Map
中存放键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从Map
中删除。WeakHashMap
能够节约存储空间,可用来缓存那些非必须存在的数据。
以下代码MapCache
类的main()
创建一个WeakHashMap
对象,它存放一组Key
对象的弱引用,此外main()
还创建一个数组对象,它存放部分Key
对象的强引用。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
62package weak;
import java.util.WeakHashMap;
class Key {
String id;
public Key(String id) {
this.id = id;
}
public String toString() {
return id;
}
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object r) {
return (r instanceof Key) && id.equals(((Key) r).id);
}
public void finalize() {
System.out.println("Finalizing Key " + id);
}
}
class Value {
String id;
public Value(String id) {
this.id = id;
}
public String toString() {
return id;
}
public void finalize() {
System.out.println("Finalizing Value " + id);
}
}
public class MapCache {
public static void main(String[] args) throws Exception {
int size = 1000;// 或者从命令行获得size的大小
Key[] keys = new Key[size]; // 存放键对象的强引用
WeakHashMap whm = new WeakHashMap();
for (int i = 0; i < size; i++) {
Key k = new Key(Integer.toString(i));
Value v = new Value(Integer.toString(i));
if (i % 3 == 0) {
keys[i] = k; // 使Key对象持有强引用
}
whm.put(k, v); // 使Key对象持有弱引用
}
// 催促垃圾回收器工作
System.gc();// 把CPU让给垃圾回收器线程
Thread.sleep(8000);
}
}
控制台打印1
2
3
4
5
6
7Finalizing Key 587
Finalizing Key 985
Finalizing Key 998
Finalizing Key 997
Finalizing Key 995
Finalizing Key 994
..................
结果:当执行System.gc()
后,垃圾回收器只会回收那些仅仅持有弱引用的Key
对象。id可以被3整数的Key
对象持有强引用,因此不会被回收。