Java引用详解

来源:互联网 发布:皮革防水剂 淘宝 编辑:程序博客网 时间:2024/05/16 19:34

一、对象可达性

Java虚拟机有5个不同级别的对象的可达性。

●强可达(Strongly reachable)

如果一个对象可以被一些线程直接使用而不用通过其他引用对象(reference objects),那么它就是强可达。一个新创建的对象对创建它的线程来讲就是强可达的。

这是我们知道并且一直在使用的引用类型(译注:通常被new出来的对象都是强可达的,他们的引用就是强引用)。任何通过强引用所使用的对象(在一个活动线程中)都不会被GC回收。

●软可达(Softly reachable)

如果一个对象没有强可达性,但是它可以通过一个软引用(soft reference)来使用,那么它就具有软可达性。

只有当系统需要更多内存时,GC才会回收具有软可达性的对象。在内存不足前,GC保证一定回收软可达的对象。

有可能我们会在代码中写下这么几行:“嘿,我想要把一些数据保存在内存中。但只要JVM快把内存用光的时候,就可以直接将这些东西回收并将这些引用置为null。我会在代码里面处理这种情况。”关于软引用(SoftReference)何时应该被回收的算法依赖于不同的JVM发行版本。它往往是一个跟引用(reference)的使用频率和使用间隔有关的函数。

软引用可用来实现内存敏感的高速缓存.但是具体的行为还是得依赖于JVM。并且多少跟内存回收机制有关,保障很少并且跟具体的JVM发行版本有关。为了缓存的可靠(及其他更多特性),大多数人都会选用像Ehcache而不是用软引用实现自己的缓存。但在一些场合,使用软引用确实可以让代码非常优雅、简洁。

●弱可达(Weakly reachable)

如果一个对象既没有强可达性,也没有软可达性,但是它可以通过一个弱引用(weak reference)来使用,那么他就具有弱可达性。当弱引用指向的弱可达对象没有其他的引用,那么这个对象就会被回收。

弱引用不能阻止垃圾回收机制清理他指向的引用。弱引用最常见的使用情景是通过WeakHashMap。它是一种简单地将对象的生命周期跟Map中对象的索引域(key)绑定的方式。只有当WeakHashMap中的Key是强可达,也就是WeakHashMap中的数据域(Data域)的对象,在应用程序的其他地方有别的引用的时候,它里面的值才不会被回收。一旦应用程序中没有其他对WeakHashMap中对象的引用,那么它的所有的key就会变成弱可达,不需要用户的额外干预,所有WeakHashMap中的对象都会被清除。这是一种优雅地防止内存泄露的方式。

●虚可达(Phantom reachable)

如果一个对象既没有强可达性,也没有软可达性、弱可达性,他已经被终结(finalized),并且有一些虚引用(phantom reference)指向它,那么它就具有虚可达性。

虚引用(PhantomReference)指向的对象是不能被取回使用的。它的get()方法永远返回null,所以它有什么用呢?

所有的引用类型都允许在构造函数中指定一个引用队列(ReferenceQueue)。从语义上讲一个虚引用(PhantomReference)以什么方式、何时入队让对象终结(finalization)以一种更好、更健壮的方式进行。

●不可达(Unreachable)

当一个对象不能通过以上的方式指向,那么这个对象就变得不可达,并因此适合被回收。

二、可达性分析算法

在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。


 

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI(即一般说的Native方法)引用的对象。

三、引用的概念及应用

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

●强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

●软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。 

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。 

为什么要使用SoftReference?在不使用SoftReference的时候,程序所需要的数据要么保存在内存中,如果数据量比较大的话,会占用比较多的内存;要么保存在介质中,需要时进行加载,在性能上肯定比不上直接读取内存。SoftReference算是比较折中的一种解决方案,它先将数据保存在内存中,在内存不足的情况下,才将数据回收。软引用可用来实现内存敏感的高速缓存.但是具体的行为还是得依赖于JVM。并且多少跟内存回收机制有关,保障很少并且跟具体的JVM发行版本有关。为了缓存的可靠(及其他更多特性),大多数人都会选用像Ehcache而不是用软引用实现自己的缓存。

 示例:

大数据对象:照片Image

Java代码 
  1. /** 
  2.  * 图片,包含大量数据 
  3.  * @author Grucee 
  4.  */  
  5. public class Image {  
  6.     private byte[][] buffer;  
  7.     private int id;   
  8.     public Image(int id, byte[][] data) {  
  9.         this.id = id;  
  10.         buffer = data;  
  11.     }     
  12.     public int getId() {  
  13.         return this.id;  
  14.     }     
  15.     public String toString() {  
  16.         return "image[" + this.id + "]";  
  17.     }  
  18. }  

 缓存管理ImageCacheManager

Java代码 
  1. /** 
  2.  * 图片缓存 
  3.  *  
  4.  * @author Grucee 
  5.  */  
  6. public class ImageCacheManager {  
  7.     private static ImageCacheManager imageCache = null;  
  8.     private ImageCacheManager() {}  
  9.     /** 
  10.      * 作为一个管理器,当然要单例了,不允许随便new一个就当做管理器 
  11.      * @return 
  12.      */  
  13.     public static ImageCacheManager instance() {  
  14.         if (imageCache == null) {  
  15.             synchronized (ImageCacheManager.class) {  
  16.                 if (imageCache == null) {  
  17.                     imageCache = new ImageCacheManager();  
  18.                 }  
  19.             }  
  20.         }  
  21.   
  22.         return imageCache;  
  23.     }  
  24.   
  25.     // 保存数据的地方  
  26.     private Map<Integer, SoftReference<Image>> cache = new HashMap<Integer, SoftReference<Image>>();  
  27.   
  28.     public void put(Image image) {  
  29.         cache.put(image.getId(), new SoftReference<Image>(image));  
  30.     }  
  31.   
  32.     private Image loadImage(int id) {  
  33.         byte[][] data = new byte[1024][1024];  
  34.         data[1023][1023] = 1;  
  35.         return new Image(id, data);  
  36.     }  
  37.   
  38.     public Image get(int id) {  
  39.         SoftReference<Image> ref = cache.get(id);  
  40.         // 没有放入过缓存,第一次加载  
  41.         if (ref == null) {  
  42.             Image loadFirst = loadImage(id);  
  43.             // 放入缓存  
  44.             put(loadFirst);  
  45.             return loadFirst;  
  46.         }  
  47.   
  48.         // 下面的情况是该图片已经被加载过,但是可能由于内存不足,又被回收了  
  49.         // 为了便于理解,使用了if else分支方式,其实else不是必须的。  
  50.         // 这两个分支,不管哪一个返回的都是对image的强引用  
  51.         Image cachedImage = ref.get();  
  52.         if (cachedImage == null) {  
  53.             // 从真实介质中读取(此处模拟这个操作)  
  54.             Image imageGetFromMedia = loadImage(id);  
  55.             // 缓存起来  
  56.             put(imageGetFromMedia);  
  57.   
  58.             System.out.println("get image:" + id + " from media.");  
  59.             return imageGetFromMedia;  
  60.         } else {  
  61.             System.out.println("get image:" + id + " from cache.");  
  62.             return cachedImage;  
  63.         }  
  64.     }  
  65. }  

 测试类

Java代码 
  1. /** 
  2.  * 测试类 
  3.  * @author Grucee 
  4.  */  
  5. public class SoftReferenceTest   
  6. {  
  7.     private ImageCacheManager cacheMgr = ImageCacheManager.instance();  
  8.     /** 
  9.      * 设置启动参数 
  10.      * @param args 
  11.      */  
  12.     public static void main(String[] args) {  
  13.         SoftReferenceTest tester = new SoftReferenceTest();  
  14.         //先把数据load进来  
  15.         for (int i = 0; i < 10; i++) {  
  16.             tester.cacheMgr.get(i);  
  17.         }  
  18.           
  19.         //一张特殊的图片,这里保存了一个对这张图片的强引用,所以该对象不会被回收  
  20.         Image myPhoto = tester.cacheMgr.get(7);  
  21.         System.out.println("--------------------------");  
  22.         System.out.println(myPhoto);  
  23.         System.out.println("--------------------------");  
  24.           
  25.         //打印图片  
  26.         for (int i = 0; i < 10; i++) {  
  27.             tester.printImage(i);  
  28.         }  
  29.     }  
  30.       
  31.     public void printImage(int id) {  
  32.         System.out.println(cacheMgr.get(id));  
  33.     }  
  34. }  

 启动设置内存堆大小为10M

-Xmx10M -Xms10M -verbose:gc

测试一:

当测试类SoftReferenceTest中特殊图片是第8张是,运行结果

[GC (Allocation Failure)  2048K->1972K(9728K), 0.0048794 secs]

get image:8from cache.

--------------------------

image[8]

--------------------------

[GC (Allocation Failure)  2898K->3066K(9728K), 0.0019205 secs]

get image:0from media.

image[0]

get image:1from media.

image[1]

[GC (Allocation Failure)  5114K->5338K(9728K), 0.0007850 secs]

[Full GC (Ergonomics)  5338K->4866K(9728K), 0.0032548 secs]

get image:2from media.

image[2]

get image:3from media.

image[3]

[GC (Allocation Failure)  6914K->7114K(8704K), 0.0006606 secs]

[Full GC (Ergonomics)  7114K->6907K(8704K), 0.0028911 secs]

get image:4from media.

image[4]

[Full GC (Ergonomics)  7931K->7912K(8704K), 0.0075340 secs]

get image:5from media.

image[5]

[GC (Allocation Failure)  3014K->3022K(9216K), 0.0004105 secs]

get image:6from media.

image[6]

[GC (Allocation Failure)  4045K->4118K(9216K), 0.0011056 secs]

get image:7from media.

image[7]

get image:8from cache.

image[8]

[GC (Allocation Failure)  5141K->5262K(9216K), 0.0005411 secs]

get image:9from media.

image[9]

 

测试二:

当测试类SoftReferenceTest中特殊图片是第7张是,运行结果

[GC (Allocation Failure)  2048K->1972K(9728K), 0.0048794 secs]

get image:7from media.

--------------------------

image[7]

--------------------------

[GC (Allocation Failure)  2898K->3066K(9728K), 0.0019205 secs]

get image:0from media.

image[0]

get image:1from media.

image[1]

[GC (Allocation Failure)  5114K->5338K(9728K), 0.0007850 secs]

[Full GC (Ergonomics)  5338K->4866K(9728K), 0.0032548 secs]

get image:2from media.

image[2]

get image:3from media.

image[3]

[GC (Allocation Failure)  6914K->7114K(8704K), 0.0006606 secs]

[Full GC (Ergonomics)  7114K->6907K(8704K), 0.0028911 secs]

get image:4from media.

image[4]

[Full GC (Ergonomics)  7931K->7912K(8704K), 0.0075340 secs]

get image:5from media.

image[5]

[GC (Allocation Failure)  3014K->3022K(9216K), 0.0004105 secs]

get image:6from media.

image[6]

[GC (Allocation Failure)  4045K->4118K(9216K), 0.0011056 secs]

get image:7from cache.

image[7]

get image:8from media.

image[8]

[GC (Allocation Failure)  5141K->5262K(9216K), 0.0005411 secs]

get image:9from media.

image[9]

测试一和测试二对比

首先我们通过启动参数-Xmx10M -Xms10M -verbose:gc,将jvm的堆内存大小设置为10M。

现在我们有10张照片,每张1M大小。由于JVM堆中还有方法区(存放类定义)、常量池等,所以这10张照片是不能够全部存放在cache中的。

通过上面测试一和测试二的对比我们可以看出,当全部加载10张图片后,我们获取第八张图片是从cache中取的;获取第七张图片时,是重新加载的(从media中加载)。所以实际上,由于内存有限,我们的cache只是在内存中保存了2张照片;之前加载的照片由被回收了,通过[GC (Allocation Failure)  6914K->7114K(8704K), 0.0006606 secs]可以看出进行了内存回收。

所以,我们这里实现的cache在内存不足的时候,会自动被回收的。

之后我们强引用了一张图片,可以看出存在强引用的这张图片是一直存在内存中的,不会被回收。

   

●弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。

对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。

示例:

public staticvoid main(String[] args)

   {

      Map<Integer, String> map = new WeakHashMap<Integer, String>();

      Integer key = new Integer(1);

      map.put(key"test");

     

      // key不再有强引用

      key = null;

      System.gc();

     

      //等待一段时间,进行垃圾回收

      try {

         Thread.sleep(10000);

      } catch (InterruptedException e) {

         e.printStackTrace();

      }

     

      System.out.println(map.size());

   }

输出:

[GC (Allocation Failure)  509K->520K(130560K), 0.0028151 secs]

[GC (System.gc())  746K->560K(131072K), 0.0007948 secs]

[Full GC (System.gc())  560K->513K(131072K), 0.0091938 secs]

0

可见当WeakHashMap中key不存在强引用时,随时都会从map中移除。  

●虚引用(PhantomReference)

1、 基本概念

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 

ReferenceQueue queue = new ReferenceQueue ();                      

PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 

2、析构finalization

Java中内存管理是自动进行的。开发者无需关心已经释放的对象的内存回收。这样的一个缺点是开发者无法得知某个特定对象何时被回收;另外,开发者无法控制内存管理。不过,java.lang.ref包定义了一些可以和垃圾回收器进行有限交互的类。具体的类是SoftReference,WeakReference以及PhantomReference,它们是Reference的子类,以不同的方式和垃圾回收器交互。

有时需要在对象被回收前做一些清理工作,可以使用析构方法。该特性可以用来回收对象相关的本地资源。但是,Java中的析构方法有很多的问题。

第一:我们无法预期finalize()何时会被调用。没有任何保证某个对象一定会被垃圾回收。一个在JVM整个生命周期中可达的对象永远不会被回收;也有可能,在对象变成可回收之后和JVM停止之前,垃圾回收线程没有运行。

第二:Java析构方法会将应用程序拖慢。管理重载了finalize()方法的对象需要耗费JVM更多的资源。

第三:对象即使被调用了finalize()方法,也无法保证该对象一定会被回收;并且,可以在finalize()方法中对对象进行拯救(重新赋予强引用)。

虽然有可达性分析算法来判定对对象状态,但这并不是对象是否被回收的条件,对象回收的条件远远比这个复杂,比如无法通过ROOT找到的对象,也不一定会回收,会进入一个死缓的阶段,那些无法通过根节点引用链找到的对象,会被第一次标记,并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize() 方法已经被虚拟机调用过,虚拟机都视为没必要执行

如果该对象被判定为有必要执行finalize(),那么对象会被放置在一个F-Queue 的队列中,并由一个优先级较低的Finalizer线程去执行,这里的执行是 JVM 会触发这个方法,但并不保证等待他运行结束,因为finalize() 方法执行慢,或者死循环,会影响该队列其他元素执行。

执行finalize() 方法就会进行第二次标记,然后等待JVM 进行回收了,而在finalize() 方法执行的同时,可以对对象进行拯救,也就是说在执行方法内部,再次对对象进行引用,那么对象就复活了。

示例:
Java代码 
  1. public class SaveFinalizedObject {  
  2.     private static class User {  
  3.         public static Resource res = null;  
  4.     }  
  5.     private static class Resource {  
  6.         @Override  
  7.         protected void finalize() throws Throwable {  
  8.             super.finalize();  
  9.             System.out.println("执行finalize方法,并拯救对象");  
  10.             User.res = this;  
  11.         }  
  12.     }  
  13.     public static void main(String[] args) throws InterruptedException {  
  14.         // 使用者占用资源  
  15.         User.res = new Resource();  
  16.         // 使用者释放资源,这时上面new的Resource对象已经没有强引用,可以被垃圾回收  
  17.         User.res = null;  
  18.   
  19.         // 此处如果执行了finalize方法后,对象即可被拯救成功,又有了强引用  
  20.         System.gc();  
  21.         Thread.sleep(500); // gc线程优先级比较低,此处等待垃圾回收  
  22.         System.out.println(User.res);  
  23.   
  24.         // 再次释放资源,让资源被回收(此时已经不能被拯救,因为finalize方法只会被调用一次)  
  25.         User.res = null;  
  26.         Thread.sleep(500);  
  27.         System.out.println(User.res);  
  28.     }  
  29. }  
 程序输出:

 

[GC (Allocation Failure)  509K->504K(130560K), 0.0008240 secs]

 

[GC (System.gc())  739K->568K(131072K), 0.0019017 secs]

 

[Full GC (System.gc())  568K->513K(131072K), 0.0067556 secs]

 

执行finalize方法,并拯救对象

 

grucee.test.Main$Resource@15db9742

 

null

 

强烈建议不要重写Objectfinalize()方法。

 

3、虚引用何时使用虚引用?

 

当理智告诉我们不应该使用finalize(由于上面一系列的缺点),可以考虑使用虚引用。该类型的引用和java.lang.ref包中定义的其他引用不同,因为它不是用来访问对象的,只是作为一个对象已经被析构、垃圾回收期准备回收它的内存的信号。

 

正如API文档中所说:

 

虚引用对象,在垃圾回收器确定它所引用的对象要被回收之后,会被放入队列。虚引用常被用来以比JAVA析构机制更灵活的方式执行清理动作。

虚引用是一种比较安全的知道对象已经从内存中移除的方式。例如,考虑一个处理大图片的应用程序。我们希望内存中已经存在的大图片在被垃圾回收期准备回收后,才去加载新的大图片。这种情况下,虚引用是一种灵活、安全的选择。旧的图片对象在析构后,会被放在ReferenceQueue引用队列中;在队列中可获取该引用后,我们就可以加载新的大图片到内存中了。

示例:

 

Java代码 
  1. /** 
  2.  * 虚引用测试用例 
  3.  * @author Grucee 
  4.  */  
  5. public class PhantomReferenceTest {  
  6.   
  7.     private static class Connection {  
  8.         private int resourceId;  
  9.         private Connection(int resourceId) {  
  10.             System.out.println("分配资源:" + resourceId);  
  11.             this.resourceId = resourceId;  
  12.         }  
  13.               
  14.         public int getResourceId() {  
  15.             return this.resourceId;  
  16.         }  
  17.     }  
  18.       
  19.     private static class ConnectionMonitor extends PhantomReference<Connection> {  
  20.         private int resourceId;  
  21.         private ReferenceQueue<Connection> queue;  
  22.         public ConnectionMonitor(Connection referent, ReferenceQueue<Connection> queue) {  
  23.             super(referent, queue);  
  24.             this.resourceId = referent.getResourceId();  
  25.             this.queue = queue;  
  26.         }  
  27.           
  28.         public void waitToCleanUP() {  
  29.             try {  
  30.                 //此处会阻塞一直到被引用的connection被回收  
  31.                 queue.remove();  
  32.                 this.clear();  
  33.                 //资源清理  
  34.                 System.out.println("清理资源:" + this.resourceId);  
  35.             } catch (InterruptedException e) {  
  36.                 e.printStackTrace();  
  37.             }  
  38.         }  
  39.     }  
  40.       
  41.     /** 
  42.      * 上个连接的资源回收之后,才允许分配下一个连接 
  43.      * @author Grucee 
  44.      */  
  45.     public static class ConnectioinManager {  
  46.         private static ConnectionMonitor monitor = null;  
  47.         private ConnectioinManager(){}  
  48.         public synchronized static Connection openConnection(int resourceId) {  
  49.             if (monitor != null) {  
  50.                 monitor.waitToCleanUP();  
  51.             }  
  52.               
  53.             Connection conn = new Connection(resourceId);  
  54.             ReferenceQueue<Connection> queue = new ReferenceQueue<Connection>();  
  55.             monitor = new ConnectionMonitor(conn, queue);  
  56.             return conn;  
  57.         }  
  58.     }  
  59.       
  60.     public static void main(String[] args) {  
  61.         Connection conn = ConnectioinManager.openConnection(101);  
  62.         //如果不添加这一行,会发现一直得不到102资源  
  63.         conn = null;  
  64.         System.gc();  
  65.         conn = ConnectioinManager.openConnection(102);  
  66.     }  
  67. }  

 

1 0
原创粉丝点击