java中SoftReference与WeakReference应用于高速缓存示例

前言:

本文首先介绍强引用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四种引用 强引用,软引用,弱引用,虚引用(转)


下一篇:Cypress web自动化6- Assertions断言使用(should, expect)