1. 程式人生 > >java中SoftReference與WeakReference應用於高速緩存示例

java中SoftReference與WeakReference應用於高速緩存示例

contains 運行時間 error nbsp 內存回收 暫時 cto 對象引用 font

前言:

本文首先介紹強引用StrongReference、軟引用SoftReference、弱引用WeakReference與虛引用PhantomReference之間的區別與聯系;

並通過一個高速緩存的構建方案,來了解SoftReference的應用場景。

本文參考書籍Thinking in Java以及多篇博文。


一、Reference分類

Reference即對象的引用,根據引用的不同類型,對JVM的垃圾回收有不同的影響。

1. 強引用StrongReference

通常構建對象的引用都是強引用,例如

Student stu = new Student();

stu就是對這個新實例化的Student對象的強引用。

當對象根節點可及(reachable),且存在強引用(棧 或者 靜態存儲區)指向該對象時,GC無法回收該對象內存,直至內存不足發生了OOM(out of memory Error):

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

當該對象未被引用了,才會被GC回收。

2. 軟引用SoftReference

軟引用通過SoftReference實例構建對其他對象的引用。不同於強引用,當JVM內存不足即將發生OOM時,在GC過程若對象根節點可及、不存在強引用指向該對象、且存在軟引用指向該對象,則該對象會被GC回收

Student stu = new Student();
SoftReference<Student> softRef = new SoftReference<Student>(stu);
stu = null;
/*此時若發生GC,Student對象只有一個軟引用softRef指向它,若內存此時即將OOM,則該Student實例將被回收*/

若SoftReference構造方法傳入了ReferenceQueue,則在回收該對象之後,相應的SoftReference實例會被add進referenceQueue:

Student stu = new Student();
ReferenceQueue<Student> studentReferenceQue = new ReferenceQueue<Student>();
SoftReference<Student> softRef = new SoftReference<Student>(stu, studentReferenceQue );
stu = null;

/*在內存不足GC,該Student實例被回收時,SoftReference實例softRef將被add進referenceQueue*/

//SoftReference<Student> softRefFromQueue = (SoftReference<Student>)studentReferenceQue.poll();

通過從referenceQueue中poll出Reference對象,即可知softReference所引用的Student對象已經被回收了。

3. 弱引用WeakReference

弱引用級別比軟引用更低。當對象根節點可及、無強引用和軟引用、有弱引用指向對象時,若發生GC,該對象將直接被回收:

Student stu = new Student();
ReferenceQueue<Student> studentReferenceQue = new ReferenceQueue<Student>();
WeakReference<Student> softRef = new WeakReference<Student>(stu, studentReferenceQue ); 
stu = null; 
/*此時發生GC*/
System.gc();
/*則Student實例將被直接回收,且WeakReference實例將被加入studentReferenceQue中*/

/*通過從studentReferenceQue中poll出Reference對象,即可知Student實例已經被回收*/
//Reference<Student> studentRef = (Reference<Student>)studentReferenceQue.poll();

4. 虛引用PhantomReference

虛引用對對象的聲明周期不產生任何影響,對JVM無任何內存回收的暗示。

其使用主要用於跟蹤對象的回收情況。

二、Reference應用:高速緩存構建

當某一類數據數量巨大,存於數據庫或者文件中,運行內存不足以承受加載全部數據的開銷時,緩存是一個比較好的方案。

根據程序的局部性原理,某一時刻使用的數據,在短時間內被使用的概率比較大。因此我們可以在使用某條數據時將其從數據庫/硬盤上加載進內存。

但是隨著程序運行時間變久,緩存也越來越多,將會對內存消耗影響不斷增大,因此也需要構建機制將老的緩存數據清除,減小緩存對進程內存占用的影響。

通過軟引用SoftReference構建緩存是個比較好的方案,正常使用時,數據被加載進內存並由SoftReference引用;當內存不足時,GC會將SoftReference引用的對象回收,從而達到保護內存的目的。

下面是一個高速緩存的案例:

首先是緩存對象數據結構Student:

class Student {
    /*Fields*/
    private String studentNumber;
    private String name;
    private int age;

    public String getStudentNumber() {
        return studentNumber;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Student(String studentNumber, String name, int age) {
        this.studentNumber = studentNumber;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "studentNumber=‘" + studentNumber + ‘\‘‘ +
                ", name=‘" + name + ‘\‘‘ +
                ", age=" + age +
                ‘}‘;
    }
}

下面是緩存類:

public class StudentCache {

    /*Constructor*/
    public StudentCache() {
        studentCacheHashMap = new HashMap<String, StudentReference>();
        studentReferenceQueue = new ReferenceQueue<Student>();
    }

    /* for student cache*/
    private HashMap<String, StudentReference> studentCacheHashMap;
    /* for GC trace */
    private ReferenceQueue<Student> studentReferenceQueue;


    /*Singleton*/
    public static StudentCache getInstance()
    {
        return InnerClassStudentCache._INSTANCE;
    }

    private static class InnerClassStudentCache
    {
        public static final StudentCache _INSTANCE = new StudentCache();
    }

    /*Cache Interface*/
    public Student getCachedStudent(String studentNumber)
    {
        cleanGCedCache();
        //不存在該Student緩存
        if(!studentCacheHashMap.containsKey(studentNumber))
        {
            //構造Student實例
            /*從數據庫中讀取該student信息,然後構造Student。此處為了方便,使用測試類StudentDataSource作為輔助*/
            Student stu = StudentDataSource.getStudent(studentNumber);
            if(null == stu)
                return null;
            String studentNum = stu.getStudentNumber();
            String name = stu.getName();
            int age = stu.getAge();
            Student student = new Student(studentNumber, name, age);

            //通過Reference加入緩存
            StudentReference studentReference = new StudentReference(student, studentReferenceQueue);
            studentCacheHashMap.put(studentNum, studentReference);
        }
        //從緩存中獲取StudentReference,並獲取Student強引用作為返回值
        return studentCacheHashMap.get(studentNumber).get();
    }

    /* clean cached students which is GCed from hashMap */

    private static class StudentReference extends SoftReference<Student>
    {
        public StudentReference(Student referent, ReferenceQueue<? super Student> q) {
            super(referent, q);
            this.studentId = referent.getStudentNumber();
        }

        /* 在GC回收Student之後,此Reference對象被放入ReferenceQueue,加標識以識別是哪個student對象被回收 */
        public final String studentId;
    }

    private void cleanGCedCache()
    {
        StudentReference studentReference = null;
        while ((studentReference = (StudentReference)studentReferenceQueue.poll()) != null)
        {
            //將已回收的Student對象從cache中移除
            studentCacheHashMap.remove(studentReference.studentId);
            System.out.println("student " + studentReference.studentId + " has been GCed, and found in referenceQueue.");
        }
    }

    public void destroy()
    {
        // 清除Cache
        cleanGCedCache();
        studentCacheHashMap.clear();
        System.gc();
        System.runFinalization();
    }
}

緩存類說明:

1. HashMap<String, StudentReference> studentCacheHashMap 用來保存Student緩存信息。
  既然是緩存,肯定要給每個數據一個標識,這裏的key選擇Student.studentNum;
  value是SoftReference對象,之所以構建SoftReference<Student>的派生類添加字段studentNum作為域的StudentReference作為value,是因為當發生GC且student被清掉時,
  我們需要判斷出是哪個student實例被回收了,從而進一步從hashMap中清除該student實例的其他緩存信息。
  該SoftReference對象引用了真正的Student對象,除了該軟引用之外,沒有其他引用指向Student對象,從而可以在內存不足OOM前回收這些student對象,釋放出內存供使用。

2. 查詢緩存時,若hashMap中沒有相應studentNum的Student對象緩存,那麽就加載student信息並新構建Student對象通過SoftReference引用存入hashMap。
  若hashMap中已存在該student信息,那麽證明緩存已經存在,直接通過SoftReference獲取Student的強引用作為返回值。

3. StudentCache對象通過靜態內部類的方式構造單例進行管理,保證線程安全。

4. 當StudentCache緩存需要清除時,調用destroy方法,清除hashMap中對student對象引用的SoftReferences。


測試類:
//數據源,代表數據庫
class StudentDataSource { static HashMap<String, Student> students; static { students = new HashMap<String, Student>(); students.put("SX1504001", new Student("SX1504001", "ZhangSan", 25)); students.put("SX1504002", new Student("SX1504002", "LiSi", 25)); students.put("SX1504003", new Student("SX1504003", "WangWu", 25)); students.put("SX1504004", new Student("SX1504004", "LiuLiu", 25)); students.put("SX1504005", new Student("SX1504005", "AAAAAA", 25)); students.put("SX1504006", new Student("SX1504006", "BBBBBB", 25)); students.put("SX1504007", new Student("SX1504007", "CCCCCC", 25)); students.put("SX1504008", new Student("SX1504008", "DDDDDD", 25)); students.put("SX1504009", new Student("SX1504009", "EEEEEE", 25)); students.put("SX1504010", new Student("SX1504010", "FFFFFF", 25)); //..... } static Student getStudent(String studentNum) { return students.get(studentNum); } }

為了方便演示緩存效果,我們將StudentReference的基類SoftReference暫時改為WeakReference,從而達到每次GC都直接將其回收的效果,方便觀察。
並構建以下測試用例:

class Tester
{
    public static void main(String[] args) {
        //通過cache訪問student
        AccessOneStudentFromCache("SX1504001");

        //假設JVM某刻自動GC了
        System.gc();
        sleep();

        //再次通過cache訪問student
        AccessOneStudentFromCache("SX1504002");
    }

    static void AccessOneStudentFromCache(String studentNum)
    {
        StudentCache studentCache = StudentCache.getInstance();
        System.out.println(“Now access student:” + studentCache.getCachedStudent(studentNum));
    }

    static void sleep()
    {
        try{
           Thread.currentThread().sleep(10);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 

運行結果如下:

Now access student:Student{studentNumber=‘SX1504001‘, name=‘ZhangSan‘, age=25}
student SX1504001 has been GCed, and found in referenceQueue.
Now access student:Student{studentNumber=‘SX1504002‘, name=‘LiSi‘, age=25}

Process finished with exit code 0

可以看到,在GC之後,WeakReference指向的SX1504001 Student對象已經被回收了。

同理,在StudentReference的基類為SoftReference<Student>時,當OOM發生時,緩存中的所有student實例將被釋放。

java中SoftReference與WeakReference應用於高速緩存示例