Java 关于引用形式(SoftReference, WeakReference, PhantomReference)

博客首页 » Java 关于引用形式(SoftReference, WeakReference, PhantomReference)

发布于 22 Jun 2014 15:14
标签 blog
关于java中的引用(WeakReference,SoftReference,PhantomReference)

http://zhang-xzhi-xjtu.iteye.com/blog/413159

深入理解ReferenceQueue GC finalize Reference

博客分类: java基础
多线程编程JVMIBMthread
关于对象如何销毁以及finalize更详细的信息

目录

概述
1 先看一个对象finalize的顺序问题。
2 对象再生及finalize只能执行一次
3 SoftReference WeakReference
4 PhantomReference
5 ReferenceQueue
Q&A

概述

先说一些基本的东西,GC只负责对象内存相关的清理,其他资源如文件句柄,db连接需要手动清理,以防止系统资源不足崩溃。System.gc()只是建议jvm执行GC,但是到底GC执行与否是由jvm决定的。

一个正常的对象的生命周期。

当新建一个对象时,会置位该对象的一个内部标识finalizable,当某一点GC检查到该对象不可达时,就把该对象放入finalize queue(F queue),GC会在对象销毁前执行finalize方法并且清空该对象的finalizable标识。

简而言之,一个简单的对象生命周期为,Unfinalized Finalizable Finalized Reclaimed。

Reference中引用的object叫做referent。

1 先看一个对象finalize的顺序问题。

Java代码 收藏代码
public class A {
B b;
public void finalize() {
System.out.println("method A.finalize at " + System.nanoTime());
}
}

public class B {
public void finalize() {
System.out.println("method B.finalize at " + System.nanoTime());
}
}

A a = new A();
a.b = new B();
a = null;
System.gc();

按照http://java.sun.com/developer/technicalArticles/javase/finalization/
所说,对象a在finalize之前会保持b的引用,但是实验中对象a和a中的对象b的finalize方法运行时间有先有后,而且大部分时间里,a的finalize方法的执行时间是晚于b的finalize方法的。我记着java编程语言书中说是一切可以finalize的对象的finalize方法的执行顺序是不确定的。到底应该听谁的?最好的实践就是不要依赖finalize的顺序或者写一些防御代码。

【note】我仍然坚持最好的实践就是不要依赖finalize的顺序或者写一些防御代码。但是通过进一步的学习和实验,因为a有可能复活,所以在a没有决定到底复活不复活之前b是不会被回收的。控制台的顺序问题应该是多线程的问题导致的。
【note】查看了JLS后,确定了finalize是乱序执行的。

2 对象再生及finalize只能执行一次

Java代码 收藏代码
public class B {

static B b;

public void finalize() {
System.out.println("method B.finalize");
b = this;
}
}

B b = new B();
b = null;
System.gc();
B.b = null;
System.gc();

对象b本来已经被置null,GC检查到后放入F queue,然后执行了finalize方法,但是执行finalize方法时该对象赋值给一个static变量,该对象又可达了,此之谓对象再生。

后来该static对象也被置null,然后GC,可以从结果看到finalize方法只运行了1次。为什么呢,因为第一次finalize运行过后,该对象的finalizable置为false了,所以该对象即使以后被gc运行,也不会执行finalize方法了。

很明显,对象再生是一个不好的编程实践,打乱了正常的对象生命周期。但是如果真的需要这么用的话,应该用当前对象为原型重新生成一个对象使用,这样以后这个新的对象还可以被GC运行finalize方法。

3 SoftReference WeakReference

SoftReference会尽量保持对referent的引用,直到JVM内存不够,才会回收SoftReference的referent。所以这个比较适合实现一些cache。

WeakReference不能阻止GC对referent的处理。

4 PhantomReference

幻影引用,幽灵引用,呵呵,名字挺好听的。

奇特的地方,任何时候调用get()都是返回null。那么它的用处呢,单独好像没有什么大的用处,所以要结合ReferenceQueue。

5 ReferenceQueue

ReferenceQueue WeakReference PhantomReference都有构造函数可以传入ReferenceQueue来监听GC对referent的处理。

Java代码 收藏代码
public class A {
}

ReferenceQueue queue = new ReferenceQueue();
WeakReference ref = new WeakReference(new A(), queue);
Assert.assertNotNull(ref.get());

Object obj = null;
obj = queue.poll();
Assert.assertNull(obj);

System.gc();

Assert.assertNull(ref.get());
obj = queue.poll();
Assert.assertNotNull(obj);

分析,在GC运行时,检测到new A()生成的对象只有一个WeakReference引用着,所以决定回收它,首先clear WeakReference的referent,然后referent的状态为finalizable,同时或者一段时间后把WeakReference放入监听的ReferenceQueue中。

注意有时候最后的Assert.assertNotNull(obj);有时会失败,因为还没有来的及把WeakReference放入监听的ReferenceQueue中。

换成PhantomReference试试,

Java代码 收藏代码
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(new A(), queue);

Assert.assertNull(ref.get());

Object obj = null;
obj = queue.poll();

Assert.assertNull(obj);

System.gc();

Thread.sleep(10000);

System.gc();

Assert.assertNull(ref.get());
obj = queue.poll();
Assert.assertNotNull(obj);

貌似和WeakReference没有什么区别呀,别急,还是有个细微的区别的,SoftReference和WeakReference在GC对referent状态改变时,先clear SoftReference/WeakReference对referent的引用,对应的referent状态为Finalizable,只是可以放入F queue,然后把SoftReference/WeakReference放入ReferenceQueue。

而PhantomReference当GC对referent的状态改变时,在把PhantomReference放入ReferenceQueue之前referent已经被GC处理到Reclaimed了,即该referent被销毁了。

搞了这么多,有什么用?可以使用PhantomReference更好的控制一些关于对象生命周期的事情,当WeakReference放入ReferenceQueue时,并不能保证该referent是被销毁了。别忘了对象可以在finalize方法里再生。而使用PhantomReference,当在ReferenceQueue中发现PhantomReference时,可以保证referent已经被销毁了。

Java代码 收藏代码
public class A {
static A a;
public void finalize() {
a = this;
}
}

ReferenceQueue queue = new ReferenceQueue();

WeakReference ref = new WeakReference(new A(), queue);

Assert.assertNotNull(ref.get());

Object obj = null;

obj = queue.poll();

Assert.assertNull(obj);

System.gc();

Thread.sleep(10000);

System.gc();

Assert.assertNull(ref.get());

obj = queue.poll();

Assert.assertNotNull(obj);

即使new A()出来的对象再生了,在queue中还是可以看到WeakReference。

Java代码 收藏代码
ReferenceQueue queue = new ReferenceQueue();

PhantomReference ref = new PhantomReference(new A(), queue);

Assert.assertNull(ref.get());

Object obj = null;

obj = queue.poll();

Assert.assertNull(obj);

// 第一次gc

System.gc();

Thread.sleep(10000);

System.gc();

Assert.assertNull(ref.get());

obj = queue.poll();

Assert.assertNull(obj);

A.a = null;

// 第二次gc

System.gc();

obj = queue.poll();

Assert.assertNotNull(obj);

第一次gc后,由于new A()的对象再生了,所以queue是空的,因为对象没有销毁。

当第二次gc后,new A()的对象销毁以后,在queue中才可以看到PhantomReference。

所以PhantomReference可以更精细的对对象生命周期进行监控。

Q&A

Q1:有这样一个问题,为什么UT会Fail?不是说对象会重生吗,到底哪里有问题?

Java代码 收藏代码
public class Test {

static Test t;

@Override
protected void finalize() {
System.out.println("finalize");
t = this;
}
}

public void testFinalize() {
Test t = new Test();
Assert.assertNotNull(t);
t = null;
System.gc();
Assert.assertNull(t);
Assert.assertNotNull(Test.t);
}

A: 对象是会重生不错。
这里会Fail有两个可能的原因,一个是gc的行为是不确定的,没有什么会保证gc运行。呵呵,我承认,我在console上看到东西了,所以我知道gc运行了一次。
另一个问题是gc的线程和我们跑ut的线程是两个独立的线程。即使gc线程里对象重生了,很有可能是我们跑完ut之后的事情了。这里就是时序问题了。

Java代码 收藏代码
public void testFinalize() throws Exception {
Test t = new Test();
Assert.assertNotNull(t);
t = null;
System.gc();
Assert.assertNull(t);

// 有可能fail.
Assert.assertNull(Test.t);
// 等一下gc,让gc线程的对象重生执行完。
Thread.sleep(5000);
// 有可能fail.
Assert.assertNotNull(Test.t);
}

这个ut和上面那个大同小异。

一般情况下,code执行到这里,gc的对象重生应该还没有发生。所以我们下面的断言有很大的概论是成立的。
Java代码 收藏代码
// 有可能fail.
Assert.assertNull(Test.t);

让ut的线程睡眠5秒,嗯,gc的线程有可能已经执行完对象重生了。所以下面这行有可能通过测试。
Java代码 收藏代码
Assert.assertNotNull(Test.t);

嗯,测试通过。但是没有人确保它每次都通过。所以我两处的注释都声明有可能fail。
这个例子很好的说明了如何在程序中用gc和重生的基本原则。
依赖gc会引入一些不确定的行为。
重生会导致不确定以及有可能的时序问题。
所以一般我们不应该使用gc和重生,但是能深入的理解这些概念又对我们编程有好处。

这两个测试如果作为一个TestSuite跑的话,情况又会有不同。因为第一个测试失败之后和第二个测试执行之间,gc执行了对象重生。如此,以下断言失败的概率会升高。
Java代码 收藏代码
// 有可能fail.
Assert.assertNull(Test.t);

To luliruj and DLevin
首先谢谢你们的回复,这个帖子发了好久了,竟然还有人回复。
reclaimed的问题可以参看本帖上边的URL。
关于finalize和ReferenceQueue和关系,主贴已经解释了,luliruj给出了不同的解释。
这个地方我们可以用小程序验证一下.
Java代码 收藏代码
public class Tem {

public static void main(String[] args) throws Exception {

ReferenceQueue queue = new ReferenceQueue();
// SoftReference ref = new SoftReference(new B(), queue);
// WeakReference ref = new WeakReference(new B(), queue);
PhantomReference ref = new PhantomReference(new B(), queue);
while (true) {
Object obj = queue.poll();
if (obj != null) {
System.out.println("queue.poll at " + new Date() + " " + obj);
break;
}
System.gc();
System.out.println("run once.");
}

Thread.sleep(100000);
}

}

class B {

@Override
protected void finalize() throws Throwable {
System.out.println("finalize at " + new Date());
}
}

在classB的finalize上打断点,然后让ref分别为SoftReference/WeakReference/PhantomReference,可以看到。
SoftReference/WeakReference都是不需要finalize执行就可以enqueue的。这个就否掉了luliruj所说的

当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放时, WeakReference 对象就被添加到它的 ReferenceQueue (如果后者存在的话)

PhantomReference必须等待finalize执行完成才可以enqueue。
这个正如主贴所说:
而PhantomReference当GC对referent的状态改变时,在把PhantomReference放入ReferenceQueue之前referent已经被GC处理到Reclaimed了,即该referent被销毁了。
http://lijinzhe.blog.163.com/blog/static/62147668201110179300248/

2007/11/16 09:10
前两天看db4o,看到关于db4o的对象缓存中的对象是weak reference(弱引用),这样对于用户查询出来的大量对象结果,db4o使用弱引用,这样当垃圾回收时可以自己判断并进行垃圾回收。对于weak reference不是很了解,查点资料,整理一下。
对于实现了自动垃圾回收的面向对象语言,都会有关于reference的一些实现,如java中在java.lang.ref中有下面几个类:
Reference
SoftReference
WeakReference
PhantomReference
ReferenceQueue
表明了几种引用关系。而这种引用关系和垃圾回收紧密相关。可以看看jdk中api文档中对这几个类的解释也会有一个比较清晰的认识。

1.SoftReference ,软引用对象,在响应内存需要时,由垃圾回收器决定是否清除此对象。软引用对象最常用于实现内存敏感的缓存。
假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。
软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。
此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。一般来说,weakReference我们用来防止内存泄漏,保证内存对象被VM回收。
2.WeakReference,弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。弱引用最常用于实现规范化的映射。
假定垃圾回收器确定在某一时间点上某个对象是弱可到达对象。这时,它将自动清除针对此对象的所有弱引用,以及通过强引用链和软引用,可以从其到达该对象的针对任何其他弱可到达对象的所有弱引用。同时它将声明所有以前的弱可到达对象为可终结的。在同一时间或晚些时候,它将那些已经向引用队列注册的新清除的弱引用加入队列。 softReference多用作来实现cache机制,保证cache的有效性。
3.PhantomReference,虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派 pre-mortem 清除操作。
如果垃圾回收器确定在某一特定时间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时间,它会将该引用加入队列。
为了确保可回收的对象仍然保持原状,虚引用的指示对象不能被检索:虚引用的 get 方法总是返回 null。
与软引用和弱引用不同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到达。
下面贴一些我写的例子:
ReferenceAndGCExample.java:
public class ReferenceAndGCExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
YDateProxy ob = new YDateProxy();
ob.testObj();
System.gc();
ob.testObj();
}
}
YDateProxy.java:
import java.lang.ref.WeakReference;
import java.util.Date;
public class YDateProxy {
WeakReference wr;
public YDateProxy() {
wr = new WeakReference(new YDate());
}
public void testObj() {
if (wr.get() == null)
System.out.println("obj 已经被清除了 ");
else
System.out.println("obj 尚未被清除,其信息是 " + wr.get().toString());
}

class YDate extends Date{
public void finalize() {
try {
super.finalize();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
运行结果:
obj 尚未被清除,其信息是 Fri Nov 16 09:08:26 CST 2007
obj 已经被清除了
在两次db.testObj()中,加入System.gc();这时虚拟机进行垃圾回收,弱引用对象被回收,所以第二次访问结果是obj已经被回收


本页面的文字允许在知识共享 署名-相同方式共享 3.0协议和GNU自由文档许可证下修改和再使用,仅有一个特殊要求,请用链接方式注明文章引用出处及作者。请协助维护作者合法权益。


系列文章

文章列表

  • Java 关于引用形式(SoftReference, WeakReference, PhantomReference)

这篇文章对你有帮助吗,投个票吧?

rating: 0+x

留下你的评论

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License