天天看点

java中持有引用 --- 引用包java.lang.ref Java 2 引用类使用指南

前言:

上次碰到个WeakHashMap,搞了一下,没明白, 碰到了引用包, 没太理解它的用处, 这次看JDK中Proxy的源码又碰到了,如鲠在喉, 一定要解决它

对象的回收过程:

标记阶段(此阶段, 一切工作必须停止, 如果有对象覆盖了finalize方法, 会先调用这个方法, 而暂时不做标记, 而推迟到下次gc的时候再做标记)

清理阶段(此阶段和工作可能并行进行)

这里大致为那些不懂gc的人解释一下:

标记阶段的主要任务就是寻找那些可以回收的对象(即不能通过你现有的所有引用直接或间接到达的对象), 这个阶段, 需要在JVM上面运行的程序暂停下来进行标记.

而清理阶段, 通常程序都不用暂停, 清理线程可以和程序并行执行, 因为他们所操作的内存并不相交.

引入引用包的目的, 就是把java中的常规引用标记为一种特定的引用, 在某些特定的情况下, gc可以对它进行回收, 而避免耗尽所有内存, 导致的JVM中常见的 OutOfMemoryError 错误的发生, 使得程序可以正常运转.

这种特殊的引用常用持有缓存的引用, 如将一个大图片放到内存中, 而避免从磁盘上读取, 当内存紧张时, 可以将其从内存中清除, 回收内存. 当再有需要时, 再将其读入内存.

如果是普通的java引用变量, 由于这个图片对象被引用, 所以一直都不会释放, 无法从内存中清除, 而用特殊的Reference来持有这个对象, 当你需要的时候, 你可以从Reference类中获取这个对象, 当这个对象被gc回收, Reference就会返回一个null, 这个时候, 你再加入额外的逻辑, 从磁盘中加载这个对象到内存中来.实现缓存的功能.

Reference下面有3个子类, 他们的回收时机各不一样:

SoftReference(合适做缓存)

当jvm把所有分配给他的内存都使用了,仍然不够, 那么gc就会收集软引用. 回收时(清除阶段), 软引用会被放入ReferenceQueue队列中, 并且解除它和引用对象的关联, 所以通过get()返回其所指向的引用对象时会null.

(将SoftReference放入ReferenceQueue对列是有原因的, 请看WeakHashMap部分)

WeakReference

和软引用类似, 区别在于gc发现WeakReference引用的对象时,就进行gc, 当在清除阶段时, 对象一旦清除, 就将其加入到ReferenceQueue中. 这时调用它的get()同样返回null

PhantomReference

这个和上面两个引用不同在于, 当它引用的对象一旦被gc发现,就会进行回收, 不过在清除之前,会现将其放入ReferenceQueue队列, 而不是等到引用对象的内存被清除了才放入. 它的get()方法始终返回null.

(get()方法始终返回null也是有原因的, PhantomReference常被用作finalize的一个替代品, 原因见下面的解释)

Why PhantomReference?

让通知更加精准, 即在finalize之后, 内存回收之前. 而不是只在finalize中.

用finalize的时候, 首先发现此对象不可及, 为了避免此对象在finalize方法中把this赋给一个强引用造成的复活,而错误被收集的情况, gc不会在调用finalize之后立即清除此对象, 而是会在第二次gc的时候清除其内存.当内存很少, 很快就要OutOfMemoryError发生时, 这种等待是致命的, 而虚引用就没这种缺点, 一次gc即可收集.在标记阶段, 如果发现此虚引用的对象不可及了, 那么就加入队列, 在清理阶段就一次性回收了. (注意, 标记阶段是同步的, 清理阶段是异步的)

Why ReferenceQueue?

为什么要在引用们所指向的referent被gc之后, 要把引用加入到ReferenceQueue之中呢?

想象一下, 如果你将WeakReference<key>放入Map中,而不是key直接放入map中.那么当key只被WeakReference引用时,这个对象就会被gc回收, 当key被回收之后, 其对应的也没有存在的意义了. 

怎么才能知道这个key被回收了呢? 

只有知道key被回收了, 我们就能及时的清除, 减少内存的消耗.

简单, 因为一旦key被清除, WeakReference就会被放入ReferenceQueue, 你可以从ReferenceQueue中找到它, 并将其作为key, 去Map中索引, 用remove方法将其移除.

而WeakHashMap就是帮你自动实现了这个Map<WeakReference<key>, >的类, 现在你再也不需要用WeakReference<key>作为key了, 你可以直接将key放入其中, WeakHashMap<key, >会自动为你做上述一切工作.

参考资料:

1. 理解弱引用

http://weblogs.java.net/blog/enicholas/archive/2006/05/understanding_w.html

2. 引用类使用指南

https://www.ibm.com/developerworks/cn/java/j-refs/

==============================================================

学习如何有效地使用SoftRef/WeakRef/PhantomRef

Java 2 引用类使用指南

学习如何有效地使用 SoftReference、WeakReference 和 PhantomReference

Java 2 平台引入了

java.lang.ref

包,其中包括的类可以让您引用对象,而不将它们留在内存中。这些类还提供了与垃圾收集器(garbage collector)之间有限的交互。Peter Haggar 在本文中分析了

SoftReference

WeakReference

PhantomReference

类的功能和行为,并就这些类的使用给出了一些编程风格上的建议。

当在 Java 2 平台中首次引入

java.lang.ref

包(其中包含

SoftReference

WeakReference

PhantomReference

类)时,它的实用性显然被过分夸大了。它包含的类可能是有用的,但这些类具有的某些局限性会使它们显得不是很有吸引力,而且其应用程序也将特别局限于解决一类特定的问题。

垃圾收集概述

引用类的主要功能就是能够引用仍可以被垃圾收集器回收的对象。在引入引用类之前,我们只能使用强引用(strong reference)。举例来说,下面一行代码显示的就是强引用

obj

Object obj = new Object();
      

obj

这个引用将引用堆中存储的一个对象。只要

obj

引用还存在,垃圾收集器就永远不会释放用来容纳该对象的存储空间。

obj

超出范围或被显式地指定为

null

时,垃圾收集器就认为没有对这个对象的其它引用,也就可以收集它了。然而您还需要注意一个重要的细节:仅凭对象可以被收集并不意味着垃圾收集器的一次指定运行就能够回收它。由于各种垃圾收集算法有所不同,某些算法会更频繁地分析生存期较短的对象,而不是较老、生存期较长的对象。因此,一个可供收集的对象可能永远也不会被回收。如果程序在垃圾收集器释放对象之前结束,这种情况就可能会出现。因此,概括地说,您永远无法保证可供收集的对象总是会被垃圾收集器收集。

这些信息对于您分析引用类是很重要的。由于垃圾收集有着特定的性质,所以引用类实际上可能没有您原来想像的那么有用,尽管如此,它们对于特定问题来说还是很有用的类。软引用(soft reference)、弱引用(weak reference)和虚引用(phantom reference)对象提供了三种不同的方式来在不妨碍收集的情况下引用堆对象。每种引用对象都有不同的行为,而且它们与垃圾收集器之间的交互也有所不同。此外,这几个新的引用类都表现出比典型的强引用“更弱”的引用形式。而且,内存中的一个对象可以被多个引用(可以是强引用、软引用、弱引用或虚引用)引用。在进一步往下讨论之前,让我们来看看一些术语:

  • 强可及对象(strongly reachable):可以通过强引用访问的对象。
  • 软可及对象(softly reachable):不是强可及对象,并且能够通过软引用访问的对象。
  • 弱可及对象(weakly reachable):不是强可及对象也不是软可及对象,并且能够通过弱引用访问的对象。
  • 虚可及对象(phantomly reachable):不是强可及对象、软可及对象,也不是弱可及对象,已经结束的,可以通过虚引用访问的对象。
  • 清除:将引用对象的 referent 域设置为

    null

    ,并将引用类在堆中引用的对象声明为 可结束的。

SoftReference 类

SoftReference

类的一个典型用途就是用于内存敏感的高速缓存。

SoftReference

的原理是:在保持对对象的引用时保证在 JVM 报告内存不足情况之前将清除所有的软引用。关键之处在于,垃圾收集器在运行时可能会(也可能不会)释放软可及对象。对象是否被释放取决于垃圾收集器的算法以及垃圾收集器运行时可用的内存数量。

WeakReference 类

WeakReference

类的一个典型用途就是规范化映射(canonicalized mapping)。另外,对于那些生存期相对较长而且重新创建的开销也不高的对象来说,弱引用也比较有用。关键之处在于,垃圾收集器运行时如果碰到了弱可及对象,将释放

WeakReference

引用的对象。然而,请注意,垃圾收集器可能要运行多次才能找到并释放弱可及对象。

PhantomReference 类

PhantomReference

类只能用于跟踪对被引用对象即将进行的收集。同样,它还能用于执行 pre-mortem 清除操作。

PhantomReference

必须与

ReferenceQueue

类一起使用。需要

ReferenceQueue

是因为它能够充当通知机制。当垃圾收集器确定了某个对象是虚可及对象时,

PhantomReference

对象就被放在它的

ReferenceQueue

上。将

PhantomReference

对象放在

ReferenceQueue

上也就是一个通知,表明

PhantomReference

对象引用的对象已经结束,可供收集了。这使您能够刚好在对象占用的内存被回收之前采取行动。

垃圾收集器和引用交互

垃圾收集器每次运行时都可以随意地释放不再是强可及的对象占用的内存。如果垃圾收集器发现了软可及对象,就会出现下列情况:

  • SoftReference

    对象的 referent 域被设置为

    null

    ,从而使该对象不再引用

    heap

    对象。
  • SoftReference

    引用过的

    heap

    对象被声明为

    finalizable

  • heap

    对象的

    finalize()

    方法被运行而且该对象占用的内存被释放,

    SoftReference

    对象就被添加到它的

    ReferenceQueue

    (如果后者存在的话)。

如果垃圾收集器发现了弱可及对象,就会出现下列情况:

  • WeakReference

    对象的 referent 域被设置为

    null

    ,从而使该对象不再引用

    heap

    对象。
  • WeakReference

    引用过的

    heap

    对象被声明为

    finalizable

  • heap

    对象的

    finalize()

    方法被运行而且该对象占用的内存被释放时,

    WeakReference

    对象就被添加到它的

    ReferenceQueue

    (如果后者存在的话)。

如果垃圾收集器发现了虚可及对象,就会出现下列情况:

  • PhantomReference

    引用过的

    heap

    对象被声明为

    finalizable

  • 与软引用和弱引用有所不同,

    PhantomReference

    在堆对象被释放之前就被添加到它的

    ReferenceQueue

    。(请记住,所有的

    PhantomReference

    对象都必须用经过关联的

    ReferenceQueue

    来创建。)这使您能够在堆对象被回收之前采取行动。

请考虑清单 1 中的代码。图 1 说明了这段代码的执行情况。

清单 1. 使用 WeakReference 及 ReferenceQueue 的示例代码

//Create a strong reference to an object
MyObject obj = new MyObject();                  //1
//Create a reference queue
ReferenceQueue rq = new ReferenceQueue();       //2
 
//Create a weakReference to obj and associate our reference queue
WeakReference wr = new WeakReference(obj, rq);  //3
      

图 1. 执行了清单 1 中行 //1、//2 和 //3 的代码之后的对象布局

java中持有引用 --- 引用包java.lang.ref Java 2 引用类使用指南

图 1 显示了每行代码执行后各对象的状态。行 //1 创建

MyObject

对象,而行 //2 则创建

ReferenceQueue

对象。行 //3 创建引用其引用对象

MyObject

WeakReference

对象,还创建它的

ReferenceQueue

。请注意,每个对象引用(

obj

rq

wr

)都是强引用。要利用这些引用类,您必须取消对

MyObject

对象的强引用,方法是将

obj

设置为

null

。前面说过,如果不这样做,对象

MyObject

永远都不会被回收,引用类的任何优点都会被削弱。

每个引用类都有一个

get()

方法,而

ReferenceQueue

类有一个

poll()

方法。

get()

方法返回对被引用对象的引用。在

PhantomReference

上调用

get()

总是会返回

null

。这是因为

PhantomReference

只用于跟踪收集。

poll()

方法返回已被添加到队列中的引用对象,如果队列中没有任何对象,它就返回

null

。因此,执行清单 1 之后再调用

get()

poll()

的结果可能是:

wr.get();   //returns reference to MyObject
rq.poll();  //returns null
      

现在我们假定垃圾收集器开始运行。由于

MyObject

对象没有被释放,所以

get()

poll()

方法将返回同样的值;

obj

仍然保持对该对象进行强引用。实际上,对象布局还是没有改变,和图 1 所示的差不多。然而,请考虑下面的代码:

obj = null;
System.gc();  //run the collector
      

在这段代码执行后,对象布局就如图 2 所示:

图 2. obj = null; 和垃圾收集器运行后的对象布局

java中持有引用 --- 引用包java.lang.ref Java 2 引用类使用指南

现在,调用

get()

poll()

将产生与前面不同的结果:

wr.get();   //returns null
rq.poll();  //returns a reference to the WeakReference object
      

这种情况表明,

MyObject

对象(对它的引用原来是由

WeakReference

对象进行的)不再可用。这意味着垃圾收集器释放了

MyObject

占用的内存,从而使

WeakReference

对象可以被放在它的

ReferenceQueue

上。这样,您就可以知道当

WeakReference

SoftReference

类的

get()

方法返回

null

时,就有一个对象被声明为

finalizable

,而且可能(不过不一定)被收集。只有当

heap

对象完全结束而且其内存被回收后,

WeakReference

SoftReference

才会被放到与其关联的

ReferenceQueue

上。清单 2 显示了一个完整的可运行程序,它展示了这些原理中的一部分。这段代码本身就颇具说明性,它含有很多注释和打印语句,可以帮助您理解。

清单 2. 展示引用类原理的完整程序

import java.lang.ref.*;
class MyObject
{
  protected void finalize() throws Throwable
  {
    System.out.println("In finalize method for this object: " + 
                       this);
  }
}
class ReferenceUsage
{
  public static void main(String args[])
  {
    hold();
    release();
  }
  public static void hold()
  {
    System.out.println("Example of incorrectly holding a strong " +
                       "reference");
    //Create an object
    MyObject obj = new MyObject();
    System.out.println("object is " + obj);
    //Create a reference queue
    ReferenceQueue rq = new ReferenceQueue();
    //Create a weakReference to obj and associate our reference queue
    WeakReference wr = new WeakReference(obj, rq);
    System.out.println("The weak reference is " + wr);
    //Check to see if it's on the ref queue yet
    System.out.println("Polling the reference queue returns " + 
                       rq.poll());
    System.out.println("Getting the referent from the " +
                       "weak reference returns " + wr.get());
    
    System.out.println("Calling GC");
    System.gc();
    System.out.println("Polling the reference queue returns " + 
                       rq.poll());
    System.out.println("Getting the referent from the " +
                       "weak reference returns " + wr.get());
  }
  public static void release()
  {
    System.out.println("");
    System.out.println("Example of correctly releasing a strong " +
                       "reference");
    //Create an object
    MyObject obj = new MyObject();
    System.out.println("object is " + obj);
    //Create a reference queue
    ReferenceQueue rq = new ReferenceQueue();
    //Create a weakReference to obj and associate our reference queue
    WeakReference wr = new WeakReference(obj, rq);
    System.out.println("The weak reference is " + wr);
    //Check to see if it's on the ref queue yet
    System.out.println("Polling the reference queue returns " + 
                       rq.poll());
    System.out.println("Getting the referent from the " +
                       "weak reference returns " + wr.get());
    
    System.out.println("Set the obj reference to null and call GC");
    obj = null;
    System.gc();
    System.out.println("Polling the reference queue returns " + 
                       rq.poll());
    System.out.println("Getting the referent from the " +
                       "weak reference returns " + wr.get());
  }
}
      

用途和风格

这些类背后的原理就是避免在应用程序执行期间将对象留在内存中。相反,您以软引用、弱引用或虚引用的方式引用对象,这样垃圾收集器就能够随意地释放对象。当您希望尽可能减小应用程序在其生命周期中使用的堆内存大小时,这种用途就很有好处。您必须记住,要使用这些类,您就不能保留对对象的强引用。如果您这么做了,那就会浪费这些类所提供的任何好处。

另外,您必须使用正确的编程风格以检查收集器在使用对象之前是否已经回收了它,如果已经回收了,您首先必须重新创建该对象。这个过程可以用不同的编程风格来完成。选择错误的风格会导致出问题。请考虑清单 3 中从

WeakReference

检索被引用对象的代码风格:

清单 3. 检索被引用对象的风格

obj = wr.get();
if (obj == null)
{
  wr = new WeakReference(recreateIt());  //1
  obj = wr.get();                        //2
}
//code that works with obj
      

研究了这段代码之后,请看看清单 4 中从

WeakReference

检索被引用对象的另一种代码风格:

清单 4. 检索被引用对象的另一种风格

obj = wr.get();
if (obj == null)
{
  obj = recreateIt();                    //1
  wr = new WeakReference(obj);           //2
}
//code that works with obj
      

请比较这两种风格,看看您能否确定哪种风格一定可行,哪一种不一定可行。清单 3 中体现出的风格不一定在所有情况下都可行,但清单 4 的风格就可以。清单 3 中的风格不够好的原因在于,

if

块的主体结束之后

obj

不一定是非空值。请考虑一下,如果垃圾收集器在清单 3 的行 //1 之后但在行 //2 执行之前运行会怎样。

recreateIt()

方法将重新创建该对象,但它会被

WeakReference

引用,而不是强引用。因此,如果收集器在行 //2 在重新创建的对象上施加一个强引用之前运行,对象就会丢失,

wr.get()

则返回

null

清单 4 不会出现这种问题,因为行 //1 重新创建了对象并为其指定了一个强引用。因此,如果垃圾收集器在该行之后(但在行 //2 之前)运行,该对象就不会被回收。然后,行 //2 将创建对

obj

WeakReference

。在使用这个

if

块之后的

obj

之后,您应该将

obj

设置为

null

,从而让垃圾收集器能够回收这个对象以充分利用弱引用。清单 5 显示了一个完整的程序,它将展示刚才我们描述的风格之间的差异。(要运行该程序,其运行目录中必须有一个“temp.fil”文件。

清单 5. 展示正确的和不正确的编程风格的完整程序

import java.io.*;
import java.lang.ref.*;
class ReferenceIdiom
{
  public static void main(String args[]) throws FileNotFoundException
  {
    broken();
    correct();
  }
  public static FileReader recreateIt() throws FileNotFoundException
  {
    return new FileReader("temp.fil");
  }
  public static void broken() throws FileNotFoundException
  {
    System.out.println("Executing method broken");
    FileReader obj = recreateIt();
    WeakReference wr = new WeakReference(obj);
    System.out.println("wr refers to object " + wr.get());
    System.out.println("Now, clear the reference and run GC");
    //Clear the strong reference, then run GC to collect obj.
    obj = null;
    System.gc();
    System.out.println("wr refers to object " + wr.get());
    //Now see if obj was collected and recreate it if it was.
    obj = (FileReader)wr.get();
    if (obj == null)
    {
      System.out.println("Now, recreate the object and wrap it 
        in a WeakReference");
      wr = new WeakReference(recreateIt());
      System.gc();  //FileReader object is NOT pinned...there is no 
                    //strong reference to it.  Therefore, the next 
                    //line can return null.
      obj = (FileReader)wr.get();
    }
    System.out.println("wr refers to object " + wr.get());
  }
  public static void correct() throws FileNotFoundException
  {
    System.out.println("");
    System.out.println("Executing method correct");
    FileReader obj = recreateIt();
    WeakReference wr = new WeakReference(obj);
    System.out.println("wr refers to object " + wr.get());
    System.out.println("Now, clear the reference and run GC");
    //Clear the strong reference, then run GC to collect obj
    obj = null;
    System.gc();
    System.out.println("wr refers to object " + wr.get());
    //Now see if obj was collected and recreate it if it was.
    obj = (FileReader)wr.get();
    if (obj == null)
    {
      System.out.println("Now, recreate the object and wrap it 
        in a WeakReference");
      obj = recreateIt();
      System.gc();  //FileReader is pinned, this will not affect 
                    //anything.
      wr = new WeakReference(obj);
    }
    System.out.println("wr refers to object " + wr.get());
  }
}
      

总结

如果使用得当,引用类还是很有用的。然而,由于它们所依赖的垃圾收集器行为有时候无法预知,所以其实用性就会受到影响。能否有效地使用它们还取决于是否应用了正确的编程风格;关键在于您要理解这些类是如何实现的以及如何对它们进行编程。