`
zhang_xzhi_xjtu
  • 浏览: 536758 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

深入理解ReferenceQueue GC finalize Reference

    博客分类:
  • java
阅读更多
关于对象如何销毁以及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的顺序问题。

	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只能执行一次

	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的处理。

	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试试,

		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已经被销毁了。

	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。

		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?不是说对象会重生吗,到底哪里有问题?

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之后的事情了。这里就是时序问题了。

	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的对象重生应该还没有发生。所以我们下面的断言有很大的概论是成立的。
		// 有可能fail.
		Assert.assertNull(Test.t);

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


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

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


To luliruj and DLevin
首先谢谢你们的回复,这个帖子发了好久了,竟然还有人回复。
reclaimed的问题可以参看本帖上边的URL。
关于finalize和ReferenceQueue和关系,主贴已经解释了,luliruj给出了不同的解释。
这个地方我们可以用小程序验证一下.
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被销毁了。
12
0
分享到:
评论
4 楼 hfhwan 2010-07-20  
zhang_xzhi_xjtu 写道
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。


我想问一下,gc是如何保证WeakReference是Finalizable,PhantomReference是reclaimed。

我看到有的文章说WeakReference是在fnalize执行之后,对象被销毁之后放入referenceQueue的。
而PhantomReference是在finalize执行之后,对象被销毁之前放入refrenceQueue的。

如果都是在finalize执行之后,那如你文章中说到的,在finalize函数中使其复活后,这个对象难道还是会被标识成Finalizable,从而被放到referenceQueue中。
这样的话不是会引起该对象不停地被gc?
3 楼 erlengleng 2009-12-10  
好文章,感谢楼主分享
2 楼 zhang_xzhi_xjtu 2009-09-19  
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。
1 楼 langyu 2009-09-18  
1.PhantomReference的目标只为了知道referent什么时候被回收了?
2.把Reference放入ReferenceQueue是有什么具体目的呢?能解决什么问题。
盼LZ明示,谢谢。

相关推荐

    Java功底之Reference

    在实际编程中,`Reference`类的使用需要谨慎,因为它涉及到对Java内存模型的深入理解。过度依赖软引用可能导致内存泄漏,而滥用虚引用可能导致不必要的复杂性。正确地使用`Reference`可以帮助优化内存使用,提高应用...

    拓胜技术专家教你如何深入理解Java四种引用类型

    为了深入理解Java内存管理,了解这四种引用类型是至关重要的。 首先,我们要了解,在JDK1.2版本之前,Java中的引用只有强引用一种,也就是说,如果一个对象有一个强引用指向它,那么这个对象就不会被垃圾收集器回收...

    详解Java对象的强、软、弱和虚引用+ReferenceQueue

    ReferenceQueue是一个引用队列,如果保存的是Reference对象本身,如果:Reference引用指向的对象被GC回收,其实Reference已经无效了这种Reference将被放入引用队列,可以在这里将其清除,避免占有空间。 六、...

    jdk 1.6 gc详解

    本文将深入探讨JDK 1.6中的GC工作原理、内存分配策略以及如何进行问题排查和调优。 首先,理解GC的基本概念,其主要任务是识别并回收那些无引用指向的对象,即所谓的"dead"对象。Hotspot JVM在内存分配上有三种主要...

    程序避免被GC 回收 --------引用

    ### 程序避免被GC回收——引用 在Java中,垃圾收集(Garbage Collection, GC)机制自动管理内存资源,有效地解决了手动管理内存时可能遇到的问题,如内存泄漏等。然而,在某些情况下,我们希望某些对象能尽可能长...

    Java引用总结--StrongReference、SoftReference、WeakReference、PhantomRef

    System.out.println("obj [Date: " + this.getTime() + "] is gc"); } public String toString() { return "Date: " + this.getTime(); } } ``` 在这个类中,我们对java.util.Date类进行了扩展,并重写了...

    Java Reference源码解析

    Java Reference源码解析 Java Reference是Java语言的一种机制,用于追踪对象的可达性和垃圾...通过了解Java Reference的源码,我们可以更好地理解Java的垃圾回收机制,并且能够更好地应用Java Reference在实际开发中。

    深入探讨 java-lang-ref 包.docx

    ReferenceQueue<Bean> queue = new ReferenceQueue(); PhantomReference<Bean> phantom = new PhantomReference(new Bean("name", 10), queue); System.gc(); // 强制执行垃圾回收 System.out.println(phantom.get()...

    全面解析Java中的GC与幽灵引用

    本文将深入探讨Java中的四种引用类型:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)以及幽灵引用(PhantomReference),并讨论它们在GC工作原理和性能调优中的作用。 1. 强引用...

    详解java中Reference的实现与相应的执行过程

    2. `Reference(T referent, ReferenceQueue<? super T> queue)`:创建一个关联指定队列的引用对象。当对象将要被回收时,引用对象会被添加到队列中,便于外部监控。 `NULL` 队列是一个特殊队列,不会保存任何数据,...

    详解java中finalize的实现与相应的执行过程

    `FinalReference`是Java内部的一个类,它是`Reference`类的子类,专为带有`finalize`方法的对象设计。每当创建一个有`finalize`方法的对象时,这个对象会被包装成一个`FinalReference`实例,并放入一个专门的引用...

    关于finalize机制和引用、引用队列的用法详解

    总结来说,理解和合理利用Java中的`finalize()`机制、引用类型和引用队列对于优化内存管理和提升程序性能至关重要。在大多数情况下,避免使用`finalize()`,而是采用显式资源管理,如使用`try-with-resources`,以及...

    java 引用相关文档

    在Java编程语言中,引用是连接对象实例与内存空间的关键概念。这个“java 引用相关...通过阅读“java 引用相关文档”,开发者可以深入理解Java引用机制,提升对内存管理的掌控能力,从而编写出更加高效、稳定的代码。

    从面试题中看java的Reference(引用)

    Java中的引用类型是Java内存管理的一个重要组成部分,特别是在理解和处理对象生命周期以及垃圾收集机制时。...在面试中能够熟练掌握和解释这些引用类型,无疑能够体现你对Java内存管理的深入理解。

    Java 中 Reference用法详解

    Java 中 Reference 用法详解 Java 中的 Reference 类型主要分为四种:强引用、软引用、弱引用和虚引用。这些引用类型在 Java 中的使用非常广泛,特别是在缓存、池化和垃圾回收机制中。 强引用(Strong Reference)...

    强,软,弱,虚1

    首先,我们来理解强引用(Strong Reference)。强引用是最常见的引用类型,它代表了程序中的普通对象引用。当一个对象被强引用指向时,该对象被认为是“可达”的,即它不会被垃圾回收器(Garbage Collector, GC)...

    Java中几个Reference常见的作用详解

    在Java编程语言中,`Reference`类...在面试中,理解这些概念可以帮助展示对Java内存管理的深入理解,也是编写高效、健壮的Java代码的基础。通过实践和示例代码,我们可以更好地掌握这些概念,并将其应用到实际项目中。

    Java中的软引用弱引用和虚引用.docx

    本文将深入探讨Java中三种特殊的引用类型——软引用(Soft Reference)、弱引用(Weak Reference)以及虚引用(Phantom Reference),并分析它们如何帮助我们更好地管理内存资源。 #### 二、基础知识回顾 在深入了解这三...

    线程池调用队列

    线程池调用队列是多线程编程中一个重要的概念,它在处理并发任务时起着关键作用。...此外,通过分析线程池的源码,可以帮助我们深入理解其内部工作机制,以便在遇到性能问题时能更精准地定位和解决。

    Java引用类型1

    虚引用主要用于对象的 finalize 回调,确保对象在 finalize 后不会被意外访问。 引用队列(ReferenceQueue)是与引用类型配合使用的工具,当弱引用或虚引用的对象被垃圾收集后,它们会自动加入到引用队列,方便程序...

Global site tag (gtag.js) - Google Analytics