关于对象如何销毁以及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的顺序问题。 

  1. public class A {
  2. B b;
  3. public void finalize() {
  4. System.out.println("method A.finalize at " + System.nanoTime());
  5. }
  6. }
  7. public class B {
  8. public void finalize() {
  9. System.out.println("method B.finalize at " + System.nanoTime());
  10. }
  11. }
  12. A a = new A();
  13. a.b = new B();
  14. a = null;
  15. 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只能执行一次 

  1. public class B {
  2. static B b;
  3. public void finalize() {
  4. System.out.println("method B.finalize");
  5. b = this;
  6. }
  7. }
  8. B b = new B();
  9. b = null;
  10. System.gc();
  11. B.b = null;
  12. 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的处理。 

  1. public class A {
  2. }
  3. ReferenceQueue queue = new ReferenceQueue();
  4. WeakReference ref = new WeakReference(new A(), queue);
  5. Assert.assertNotNull(ref.get());
  6. Object obj = null;
  7. obj = queue.poll();
  8. Assert.assertNull(obj);
  9. System.gc();
  10. Assert.assertNull(ref.get());
  11. obj = queue.poll();
  12. Assert.assertNotNull(obj);

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

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

换成PhantomReference试试, 

  1. ReferenceQueue queue = new ReferenceQueue();
  2. PhantomReference ref = new PhantomReference(new A(), queue);
  3. Assert.assertNull(ref.get());
  4. Object obj = null;
  5. obj = queue.poll();
  6. Assert.assertNull(obj);
  7. System.gc();
  8. Thread.sleep(10000);
  9. System.gc();
  10. Assert.assertNull(ref.get());
  11. obj = queue.poll();
  12. 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已经被销毁了。 

  1. public class A {
  2. static A a;
  3. public void finalize() {
  4. a = this;
  5. }
  6. }
  7. ReferenceQueue queue = new ReferenceQueue();
  8. WeakReference ref = new WeakReference(new A(), queue);
  9. Assert.assertNotNull(ref.get());
  10. Object obj = null;
  11. obj = queue.poll();
  12. Assert.assertNull(obj);
  13. System.gc();
  14. Thread.sleep(10000);
  15. System.gc();
  16. Assert.assertNull(ref.get());
  17. obj = queue.poll();
  18. Assert.assertNotNull(obj);

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

  1. ReferenceQueue queue = new ReferenceQueue();
  2. PhantomReference ref = new PhantomReference(new A(), queue);
  3. Assert.assertNull(ref.get());
  4. Object obj = null;
  5. obj = queue.poll();
  6. Assert.assertNull(obj);
  7. // 第一次gc
  8. System.gc();
  9. Thread.sleep(10000);
  10. System.gc();
  11. Assert.assertNull(ref.get());
  12. obj = queue.poll();
  13. Assert.assertNull(obj);
  14. A.a = null;
  15. // 第二次gc
  16. System.gc();
  17. obj = queue.poll();
  18. Assert.assertNotNull(obj);

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

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

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

Q&A 

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

  1. public class Test {
  2. static Test t;
  3. @Override
  4. protected void finalize() {
  5. System.out.println("finalize");
  6. t = this;
  7. }
  8. }
  9. public void testFinalize() {
  10. Test t = new Test();
  11. Assert.assertNotNull(t);
  12. t = null;
  13. System.gc();
  14. Assert.assertNull(t);
  15. Assert.assertNotNull(Test.t);
  16. }

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

  1. public void testFinalize() throws Exception {
  2. Test t = new Test();
  3. Assert.assertNotNull(t);
  4. t = null;
  5. System.gc();
  6. Assert.assertNull(t);
  7. // 有可能fail.
  8. Assert.assertNull(Test.t);
  9. // 等一下gc,让gc线程的对象重生执行完。
  10. Thread.sleep(5000);
  11. // 有可能fail.
  12. Assert.assertNotNull(Test.t);
  13. }

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

一般情况下,code执行到这里,gc的对象重生应该还没有发生。所以我们下面的断言有很大的概论是成立的。 

  1. // 有可能fail.
  2. Assert.assertNull(Test.t);

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

  1. Assert.assertNotNull(Test.t);

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

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

  1. // 有可能fail.
  2. Assert.assertNull(Test.t);

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

  1. public class Tem {
  2. public static void main(String[] args) throws Exception {
  3. ReferenceQueue queue = new ReferenceQueue();
  4. // SoftReference ref = new SoftReference(new B(), queue);
  5. // WeakReference ref = new WeakReference(new B(), queue);
  6. PhantomReference ref = new PhantomReference(new B(), queue);
  7. while (true) {
  8. Object obj = queue.poll();
  9. if (obj != null) {
  10. System.out.println("queue.poll at " + new Date() + " " + obj);
  11. break;
  12. }
  13. System.gc();
  14. System.out.println("run once.");
  15. }
  16. Thread.sleep(100000);
  17. }
  18. }
  19. class B {
  20. @Override
  21. protected void finalize() throws Throwable {
  22. System.out.println("finalize at " + new Date());
  23. }
  24. }

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

深入理解ReferenceQueue GC finalize Reference的更多相关文章

  1. 【转】深入理解Major GC, Full GC, CMS

    声明:本文转自http://blog.csdn.net/iter_zc/article/details/41825395,转载务必声明. 很多人都分不清Major GC, Full GC的概念,事实上 ...

  2. 深入理解Major GC, Full GC, CMS

    很多人都分不清Major GC, Full GC的概念,事实上我查了下资料,也没有查到非常精确的Major GC和Full GC的概念定义.分不清这两个概念可能就会对这个问题疑惑:Full GC会引起 ...

  3. 理解JVM GC

    理解JVM GC对于我们把控Java应用有很大的帮助.下面我从运维角度,把网上的JVM相关的资料整理如下,以加深对JVM GC的理解.如有错误的地方,请看官指正. JVM内存使用分类 JVM的内存分区 ...

  4. 理解CMS GC日志

    本文翻译自:https://blogs.oracle.com/poonam/entry/understanding_cms_gc_logs 准备工作 JVM的GC日志的主要参数包括如下几个:-XX:+ ...

  5. ? 这是个很好的问题。Go 当前的 GC 显然做了一些额外的工作,但它也跟其他的工作并行执行,所以在具有备用 CPU 的系统上,Go 正在作出合理的选择。请看 https://golang.org/issue/17969 结束语(Closing notes) 通过研究 Go 垃圾收集器,我能够理解 Go GC 当前结构的背景以及它如何克服它的弱点。Go发展得非常快。如果你对 Go感兴趣,最好继

    ? 这是个很好的问题.Go 当前的 GC 显然做了一些额外的工作,但它也跟其他的工作并行执行,所以在具有备用 CPU 的系统上,Go 正在作出合理的选择.请看 https://golang.org/i ...

  6. 深入理解JDK中的Reference原理和源码实现

    前提 这篇文章主要基于JDK11的源码和最近翻看的<深入理解Java虚拟机-2nd>一书的部分内容,对JDK11中的Reference(引用)做一些总结.值得注意的是,通过笔者对比一下JD ...

  7. 深入理解 Java —— GC 机制

    1. 基础知识 1.1 什么是垃圾回收? 程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理非常重要. 垃圾回收就是对这些无效资 ...

  8. JVM相关 - 深入理解 System.gc()

    本文基于 Java 17-ea,但是相关设计在 Java 11 之后是大致一样的 我们经常在面试中询问 System.gc() 究竟会不会立刻触发 Full GC,网上也有很多人给出了答案,但是这些答 ...

  9. 理解java的finalize

    基本预备相关知识 1 java的GC只负责内存相关的清理,所有其它资源的清理必须由程序员手工完成.要不然会引起资源泄露,有可能导致程序崩溃. 2 调用GC并不保证GC实际执行. 3 finalize抛 ...

  10. 深入理解Java GC

    一.概述 GC(Carbage Collection)垃圾收集器,由JVM自动回收已死亡的对象垃圾. 这也是Java与C++等语言的主要区别之一. 二.如何确认对象已死 1. 引用计数算法 引用计数法 ...

随机推荐

  1. NSmartProxy:一款.NET开源、跨平台的内网穿透工具

    前言 今天大姚给大家分享一款.NET开源.免费(MIT License).跨平台的内网穿透工具,采用.NET Core的全异步模式打造:NSmartProxy. 内网穿透工具介绍 内网穿透工具是一种能 ...

  2. spm 一阶分析的Microtime onset应该如何填写?

    1. 如果对数据进行了slice timing, 那么在进行一阶分析时应该修改microtime onset和 microtime resolution这两个参数, 假设数据的slice order= ...

  3. CPRFL:基于CLIP的新方案,破解长尾多标签分类难题 | ACM MM'24

    现实世界的数据通常表现为长尾分布,常跨越多个类别.这种复杂性突显了内容理解的挑战,特别是在需要长尾多标签图像分类(LTMLC)的场景中.在这些情况下,不平衡的数据分布和多物体识别构成了重大障碍.为了解 ...

  4. HTML & CSS – dir, direction, writing-mode, ltr (left to rigth), rtl (right to left)

    前言 世界上有很多语言的阅读方向是不同的. 英文 中文 (以前才有竖排文字, 现在中文和英语一样了) 阿拉伯文 (Arabic) 面对不同的语言, HTML 和 CSS 就需要不同的写法. 虽然我没有 ...

  5. 关于HOJ的搭建和二开经验小结

    经验在最后,先说流程. 除了HOJ,之前先装的HDU,属实难用,然后是hustOJ,嗯很好用,但架不住丑,对管理员实在不友好. 好了不闲记,进入正题: 一.流程 1.官网文档先过一遍. 2.配置好do ...

  6. Java如何解决同时出库入库订单号自动获取问题

    在Java中处理同时出库和入库的订单号自动获取问题,通常涉及到多线程环境下的并发控制.为了确保订单号的唯一性和连续性,我们可以使用多种策略,如数据库的自增ID.分布式锁.或者利用Java的并发工具类如 ...

  7. LeetCode 1388. Pizza With 3n Slices(3n 块披萨)(DP)

    给你一个披萨,它由 3n 块不同大小的部分组成,现在你和你的朋友们需要按照如下规则来分披萨: 你挑选 任意 一块披萨.Alice 将会挑选你所选择的披萨逆时针方向的下一块披萨.Bob 将会挑选你所选择 ...

  8. 八字测算api接口数据示例_奥顺八字测算接口分享

    八字测算免费api接口,每日开放时间在早上8点到晚上十点,本api接口完全免费,是奥顺居八字测算网程序内部接口,提供本地调用的,现在免费分享出来,仅供测试. 接口名称:八字精批测算api接口示例 接口 ...

  9. 使用nacos上传配置文件报错

    1.使用nacos导入配置文件报错:未读取到合法数据,请检查导入的数据文件. 对比在naocs server中导出的文件,发现是少了一级目录.需要创建一个文件夹,名称为组的名称.因为在nacos上传文 ...

  10. day06-Hello World案例

    HelloWorld 象征着作为一个程序员向世界的第一声呐喊 新建一个文件夹用来存放代码 新建一个java文件 文件后缀名为.java Hello.java [注意]系统可能没有显示文件的后缀名,需要 ...