前言:
本文首先介绍强引用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实例将被释放。