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