前言:
本文首先介紹強引用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實例將被釋放。