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