半夜ATM机前看书的那位

导航

使用 SoftReference 软引用(转)

http://www.oschina.net/question/4873_11561

Soft Ref(软引用)对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。

由于照本宣科,所以我一厢情愿的认为只要Strong Ref不可达,那么GC会自动回收Soft Ref可达的对象。正好最近项目上遇到一个旧版本DWR引起的内存泄漏(新版已修正),由于不愿更新到DWR的最新版本,所以想用Soft Ref来实现。可惜,到最后还是失败了,原因在于没正确使用Soft Ref,那么如何正确使用,在这里聊聊。

由于前文中有提到Weak Ref有个java.util.WeakHashMap实现类,所以就从它的源代码入手吧。WeakHashMap内部是一个Entry[],而 Entry是继承了WeakReference并实现Map.Entry接口的静态类,类声明:private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>。好了,由此可知,Entry实际上是WeakReference的子类,每次实例化Entry也就是在实例化 WeakReference,在构造函数中调用super(key, queue)为WeakReference传递标识(key)和ReferenceQueue实例(queue)。ReferenceQueue和 WeakReference是联合使用的,作用是当WeakReference所引用的对象被回收后,可以通过WeakReference的poll() 来得到WeakReference,但是请注意,如果再对得到的WeakReference进行get(),结果将是null,因为被Weak Ref的对象本身已经被回收。接着再看WeakHashMap的put(K key, V value)方法,该方法又关联调用了私有方法expungeStaleEntries(),expungeStaleEntries()的注释表明,该 方法是用来删除失效Entry的,这里调用了ReferenceQueue的poll()方法来找出被回收的对象(已被Weak Ref),然后清除,并缩小键-值映射关系的数目。根据观察,例如remove(Object key)、size()、get(Object key)这些经常使用的方法,内部都优先调用了expungeStaleEntries()。由此可以见,在程序运行中很可能会引起被Weak Ref的对象的回收,所以每次操作都要进行WeakReference的poll(),而后续的清除工作还得手工编码完成。

好,有了WeakHashMap的实现经验,开始实现自己的SoftReference吧。

Pilot类。

/**
 * Pilot class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.bo;

public class Pilot{
    
private String name;
    
private int age;
    
    
public String getName() {
        
return name;
    }
    
public void setName(String name) {
        
this.name = name;
    }
    
public int getAge() {
        
return age;
    }
    
public void setAge(int age) {
        
this.age = age;
    }
}


SoftRefedPilot类,模拟WeakHashMap的Entry。

/**
 * SoftRefedPilot class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.bo;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class SoftRefedPilot extends SoftReference<Pilot> {
    
public int key;
    
    
public SoftRefedPilot(int key, Pilot referent, ReferenceQueue<Pilot> q) {
        
super(referent, q);
        
this.key = key;
    }
}


测试类TestSoftReference。

/**
 * TestSoftReference class
 * 
@author rosen jiang
 
*/

package org.rosenjiang.test;

import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import org.rosenjiang.bo.Pilot;
import org.rosenjiang.bo.SoftRefedPilot;

public class TestSoftReference {

    
public static void main(String[] args) {
        soft();
    }
    
    
static void soft(){
        Map
<Integer, SoftRefedPilot> map = new HashMap<Integer, SoftRefedPilot>();
        ReferenceQueue
<Pilot> queue = new ReferenceQueue<Pilot>();
        
int i = 0;
        
while (i < 10000000) {
            Pilot p 
= new Pilot();
            map.put(i, 
new SoftRefedPilot(i, p, queue));
            
//p = null;
            SoftRefedPilot pollref = (SoftRefedPilot) queue.poll();
            
if (pollref != null) {//找出被软引用回收的对象
                
//以key为标志,从map中移除
                map.remove(pollref.key);
            }
            i
++;
        }
        System.out.println(
"done");
    }
}


好了,在JVM上加入-XX:+PrintGC参数观察GC信息吧。

[GC 55120K->54791K(65088K), 0.0307371 secs]
[GC 58887K->58558K(65088K), 0.0313663 secs]
[Full GC 62654K->52534K(65088K), 0.3171671 secs]
[GC 56630K->56301K(65088K), 0.0278301 secs]
[GC 60397K->60068K(65088K), 0.0303315 secs]
[Full GC 64164K->55894K(65088K), 0.3330122 secs]
[GC 59990K->59660K(65088K), 0.0273494 secs]
[Full GC 63756K->63179K(65088K), 0.3415388 secs]
[Full GC 64640K->43968K(65088K), 0.3204639 secs]
[GC 48064K->47735K(65088K), 0.0329379 secs]


可以看到,当heap达到64m,随即被Full GC,正如前文中说到的那样,内存吃紧的时候,Soft Ref开始进行清理,另外从主观感受和客观日志表明,在Full GC的时候,的确比一般的GC要慢得多,貌似有10倍的差距。所以,利用Soft Ref来做缓存,这个效率还得重新考虑。 

 

对象可及性的判断

在很多时候,一个对象并不是从根集直接引用的,而是一个对象被其他对象引用,甚至同时被几个对象所引用,从而构成一个以根集为顶的树形结构。如图2所示

 

  在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。由图可以看出,从根集到一个对象可以由很多条路径。比如到达对象5的路径就有①-⑤,③-⑦两条路径。由此带来了一个问题,那就是某个对象的可及性如何判断:

◆单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
◆多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。

比如,我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可及对象。

3.使用软引用构建敏感数据的缓存

3.1 为什么需要使用软引用

    首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。

3.2 如何使用软引用

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

看下面代码:

MyObject aRef = new?MyObject();
SoftReference aSoftRef=new SoftReference(aRef);

此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aReference的强引用,所以这个MyObject对象是强可及对象。

随即,我们可以结束aReference对这个MyObject实例的强引用:

aRef = null;

   此后,这个MyObject对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象。Java虚拟机的垃圾收集线程对软可及对象和其他一般Java对象进行了区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。在回收这些对象之前,我们可以通过:

MyObject anotherRef=(MyObject)aSoftRef.get();

重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了。

3.3 使用ReferenceQueue清除失去了软引用对象的SoftReference

    作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:

ReferenceQueue queue = new?ReferenceQueue();
SoftReference?ref=new?SoftReference(aMyObject, queue);

    那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。常用的方式为:

SoftReference ref = null;

while ((ref = (EmployeeRef) q.poll()) != null) {
   // 清除ref
}

理解了ReferenceQueue的工作机制之后,我们就可以开始构造一个Java对象的高速缓存器了。

3.4通过软可及对象重获方法实现Java对象的高速缓存

    利用Java2平台垃圾收集机制的特性以及前述的垃圾对象重获方法,我们通过一个雇员信息查询系统的小例子来说明如何构建一种高速缓存器来避免重复构建同一个对象带来的性能损失。我们将一个雇员的档案信息定义为一个Employee类:

public class Employee {
  private String id;// 雇员的标识号码
  private String name;// 雇员姓名
  private String department;// 该雇员所在部门
  private String Phone;// 该雇员联系电话
  private int salary;// 该雇员薪资
  private String origin;// 该雇员信息的来源

// 构造方法

public Employee(String id) { 
     this.id = id;
     getDataFromlnfoCenter();
}

// 到数据库中取得雇员信息

private void getDataFromlnfoCenter() {
  // 和数据库建立连接井查询该雇员的信息,将查询结果赋值
  // 给name,department,plone,salary等变量
  // 同时将origin赋值为"From DataBase"
}

……

   这个Employee类的构造方法中我们可以预见,如果每次需要查询一个雇员的信息。哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这是需要消耗很多时间的。下面是一个对Employee对象进行缓存的缓存器的定义:

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Hashtable;

public class EmployeeCache {
   static private EmployeeCache cache;// 一个Cache实例
   private Hashtable< String,EmployeeRef> employeeRefs;// 用于Chche内容的存储
   private ReferenceQueue< Employee> q;// 垃圾Reference的队列

   // 继承SoftReference,使得每一个实例都具有可识别的标识。
     private class EmployeeRef extends SoftReference< Employee> {
      private String _key = "";
       public EmployeeRef(Employee em, ReferenceQueue< Employee> q) {
      super(em, q);
      _key = em.getID();
    }
   }

  // 构建一个缓存器实例
    private EmployeeCache() {
      employeeRefs = new Hashtable<String,EmployeeRef>();
       q = new ReferenceQueue<Employee>();

   }

    // 取得缓存器实例

public static EmployeeCache getInstance() {
    if (cache == null) {
       cache = new EmployeeCache();
   }
    return cache;

}

  // 以软引用的方式对一个Employee对象的实例进行引用并保存该引用
   private void cacheEmployee(Employee em) {
    cleanCache();// 清除垃圾引用
     EmployeeRef ref = new EmployeeRef(em, q);
     employeeRefs.put(em.getID(), ref);
   }

   // 依据所指定的ID号,重新获取相应Employee对象的实例
    public Employee getEmployee(String ID) {
       Employee em = null;
        // 缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。
       if (employeeRefs.containsKey(ID)) {
          EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
          em = (Employee) ref.get();
       }

// 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,
// 并保存对这个新建实例的软引用

if (em == null) {
   em = new Employee(ID);
    System.out.println("Retrieve From EmployeeInfoCenter. ID=" + ID);
    this.cacheEmployee(em);
   }
   return em;
   }

private void cleanCache() {
   EmployeeRef ref = null;
   while ((ref = (EmployeeRef) q.poll()) != null) {
    employeeRefs.remove(ref._key);
   }
   }

// 清除Cache内的全部内容

public void clearCache() {
   cleanCache();
    employeeRefs.clear();
    System.gc();
    System.runFinalization();
   }

}

 

注:原来ReferenceQueue起到一个监听器的效果,当发现SoftReference.get()方法返回的是null值时,就会将SoftReference注册到自己里面队列里,当我们调用ReferenceQueue的poll()方法时,返回并删除该SoftReference。 

 

 //: SoftHashMap.java

import java.util.*;
import java.lang.ref.*;

public class SoftHashMap extends AbstractMap {
  
/** The internal HashMap that will hold the SoftReference. */
  
private final Map hash = new HashMap();
  
/** The number of "hard" references to hold internally. */
  
private final int HARD_SIZE;
  
/** The FIFO list of hard references, order of last access. */
  
private final LinkedList hardCache = new LinkedList();
  
/** Reference queue for cleared SoftReference objects. */
  
private final ReferenceQueue queue = new ReferenceQueue();

  
public SoftHashMap() { this(100); }
  
public SoftHashMap(int hardSize) { HARD_SIZE = hardSize; }

  
public Object get(Object key) {
    Object result 
= null;
    
// We get the SoftReference represented by that key
    SoftReference soft_ref = (SoftReference)hash.get(key);
    
if (soft_ref != null) {
      
// From the SoftReference we get the value, which can be
      
// null if it was not in the map, or it was removed in
      
// the processQueue() method defined below
      result = soft_ref.get();
      
if (result == null) {
        
// If the value has been garbage collected, remove the
        
// entry from the HashMap.
        hash.remove(key);
      } 
else {
        
// We now add this object to the beginning of the hard
        
// reference queue.  One reference can occur more than
        
// once, because lookups of the FIFO queue are slow, so
        
// we don't want to search through it each time to remove
        
// duplicates.
        hardCache.addFirst(result);
        
if (hardCache.size() > HARD_SIZE) {
          
// Remove the last entry if list longer than HARD_SIZE
          hardCache.removeLast();
        }
      }
    }
    
return result;
  }

  
/** We define our own subclass of SoftReference which contains
   not only the value but also the key to make it easier to find
   the entry in the HashMap after it's been garbage collected. 
*/
  
private static class SoftValue extends SoftReference {
    
private final Object key; // always make data member final
    /** Did you know that an outer class can access private data
     members and methods of an inner class?  I didn't know that!
     I thought it was only the inner class who could access the
     outer class's private information.  An outer class can also
     access private members of an inner class inside its inner
     class. 
*/
    
private SoftValue(Object k, Object key, ReferenceQueue q) {
      
super(k, q);
      
this.key = key;
    }
  }

  
/** Here we go through the ReferenceQueue and remove garbage
   collected SoftValue objects from the HashMap by looking them
   up using the SoftValue.key data member. 
*/
  
private void processQueue() {
    SoftValue sv;
    
while ((sv = (SoftValue)queue.poll()) != null) {
      hash.remove(sv.key); 
// we can access private data!
    }
  }
  
/** Here we put the key, value pair into the HashMap using
   a SoftValue object. 
*/
  
public Object put(Object key, Object value) {
    processQueue(); 
// throw out garbage collected values first
    return hash.put(key, new SoftValue(value, key, queue));
  }
  
public Object remove(Object key) {
    processQueue(); 
// throw out garbage collected values first
    return hash.remove(key);
  }
  
public void clear() {
    hardCache.clear();
    processQueue(); 
// throw out garbage collected values
    hash.clear();
  }
  
public int size() {
    processQueue(); 
// throw out garbage collected values first
    return hash.size();
  }
  
public Set entrySet() {
    
// no, no, you may NOT do that!!! GRRR
    throw new UnsupportedOperationException();
  }
}
 

 

//: SoftHashMapTest.java

import java.lang.ref.*;
import java.util.*;

public class SoftHashMapTest {
  
private static void print(Map map) {
    System.out.println(
"One=" + map.get("One"));
    System.out.println(
"Two=" + map.get("Two"));
    System.out.println(
"Three=" + map.get("Three"));
    System.out.println(
"Four=" + map.get("Four"));
    System.out.println(
"Five=" + map.get("Five"));
  }
  
private static void testMap(Map map) {
    System.out.println(
"Testing " + map.getClass());
    map.put(
"One"new Integer(1));
    map.put(
"Two"new Integer(2));
    map.put(
"Three"new Integer(3));
    map.put(
"Four"new Integer(4));
    map.put(
"Five"new Integer(5));
    print(map);
    
byte[] block = new byte[10*1024*1024]; // 10 MB
    print(map);
  }
  
public static void main(String[] args) {
    testMap(
new HashMap());
    testMap(
new SoftHashMap(2));
  }
}

posted on 2011-08-27 18:09  zhizhesky  阅读(659)  评论(0编辑  收藏  举报