java中的强,软,弱,虚引用(及利用软引用实现高速缓存)

在java中引用的类型一共有四种,分别是:强引用,软引用,弱引用和虚引用。
那么他们各自的定义是什么呢?
1.强引用(StrongReference)
强引用是使用最普通的应用。如果一个对象具有强引用,那么gc绝不会回收它。当内存空间不足,java虚拟机宁愿抛出OOM(OutOfMemory),使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
2.软引用(SoftReference):
如果一个对象只具有软引用,则内存空间足够,gc就不会回收它。如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以用来实现内存敏感的高速缓存。(下有例子)。
软引用常用的用法:软引用可以和一个应用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,java虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在gc线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否充足,都会回收它的内存。不过,由于gc是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,java虚拟机就会把这个弱引用加入到与之关联的引用队列中。这点跟软引用的常用用法没有什么不同。
4.虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue();
    PhantomReference pr = new PhantomReference(new Object(),queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
使用软引用来实现高速缓存
需要使用软引用的应用场景
我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。

我们通过一个雇员信息查询系统的小例子来说明如何构建一种高速缓存器来避免重复构建同一个对象带来的性能损失。我们将一个雇员的档案信息定义为一个Employee类:

package com.mydoctest;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class Employee {
    private String id;// 雇员的标识号码
    private String name;// 雇员姓名
    private String department;// 该雇员所在部门
    private String Phone;// 该雇员联系电话
    private int salary;// 该雇员薪资
    private String origin;// 该雇员信息的来源

    // 构造方法
    public Employee(String id) {
        this.id = id;
        getDataFromlnfoCenter();
    }

    // 到数据库中取得雇员信息
    private void getDataFromlnfoCenter() {
        //通过sleep来模拟从数据库或者文件中查信息的耗时操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("sleep error");
            e.printStackTrace();
        }
    }
    public String getID(){
        return id;
    }

}

这个Employee类的构造方法中我们可以预见,如果每次需要查询一个雇员的信息。哪怕是几秒钟之前查询过的,都要重新构建一个实例,这是需要消耗很多时间的。下面是一个对Employee对象进行缓存的缓存器的定义:

package com.mydoctest;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Hashtable;

public class EmployeeCache{
    static private EmployeeCache cache;//一个cache实例
    private Hashtable<String,EmployeeRef> employeeRefs;//用于cache内容的存储
    private ReferenceQueue<Employee> q;//垃圾reference的队列

    //继承softreference,使得每一个实例都具有可识别的标识
    //并且该标识与其在hashmap内的key相同

    private class EmployeeRef extends SoftReference<Employee>{
        private String _key = "";

        public EmployeeRef(Employee em, ReferenceQueue<? super Employee> q) {
            super(em, q);
            _key=em.getID();
        }
    }


    //构建一个缓存器实例
    private EmployeeCache(){
        employeeRefs = new Hashtable<String,EmployeeRef>();
        q = new ReferenceQueue<Employee>();
    }

    //取得缓存器实例
    public static EmployeeCache getInstance(){
        if(cache == null){
            cache = new EmployeeCache();
        }
        return cache;
    }

    //以软引用的方式对一个employee对象的实例进行引用并保存该引用
    private void cacheEmployee(Employee em){
        cleanCache();//清除垃圾引用
        EmployeeRef ref = new EmployeeRef(em,q);
        employeeRefs.put(em.getID(),ref);
    }

    //依据所指定的ID号,重新获取相应的employee对象的实例
    public Employee getEmployee(String ID){
        Employee em = null;
        //缓存中是否有该employee实例的软引用,如果有,从软引用中取得
        if(employeeRefs.containsKey(ID)){
            EmployeeRef ref = employeeRefs.get(ID);
            em = ref.get();
        }

        //如果没有软引用,或者从软引用中取得的实例是null
        //重新构建一个实例,并保存对这个新建实例的软引用
        if(em==null){
            em=new Employee(ID);
            System.out.println("retrieve from employeeinfocenter.id"+ID);
            this.cacheEmployee(em);
        }
        return em;
    }


    //清除那些所软引用的employee对象已经被回收的employeeRef对象
    private void cleanCache() {
        EmployeeRef ref = null;
        while((ref= (EmployeeRef) q.poll())!=null){
            employeeRefs.remove(ref._key);
        }
    }

    //清除cache内的全部内容
    public void clearCache(){
        cleanCache();
        employeeRefs.clear();
        System.gc();
        System.runFinalization();
    }
}




我的测试代码为:

package test3;

public class Main {

    public static void main(String[] args) {
        EmployeeCache cache = EmployeeCache.getInstance();
        System.out.println("start");
        System.out.println("k1 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k1 end:"+System.currentTimeMillis());
        System.out.println("k2 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k2 end:"+System.currentTimeMillis());
        System.out.println("k3 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k3 end:"+System.currentTimeMillis());
        System.out.println("k4 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k4 end:"+System.currentTimeMillis());
        cache.clearCache();
        System.out.println("after clear");
        System.out.println("k5 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k5 end:"+System.currentTimeMillis());
        System.out.println("k6 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k6 end:"+System.currentTimeMillis());
    }

}

下面是运行结果:

start k1 start:1453194918981 retrieve from employeeinfocenter.id1 1 k1 end:1453194921982 k2 start:1453194921982 retrieve from employeeinfocenter.id2 2 k2 end:1453194924982 k3 start:1453194924982 1 k3 end:1453194924982 k4 start:1453194924982 2 k4 end:1453194924983 after clear k5 start:1453194925010 retrieve from employeeinfocenter.id1 1 k5 end:1453194928010 k6 start:1453194928010 retrieve from employeeinfocenter.id2 2 k6 end:1453194931010 

证明确实能够缓存,并且节省了时间。
另外补充相关的对象可及性的知识。
对象可及性的判断
在很多时候,一个对象并不是从根集直接引用的,而是一个对象被其他对象引用,甚至被几个对象所引用,从而构成一个以根集为顶的树形结构。如图:
java中的强,软,弱,虚引用(及利用软引用实现高速缓存)_第1张图片

在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。由图可以看出,从根集到一个对象可以由很多条路径。比如到达对象5的路径就有①-⑤,③-⑦两条路径。由此带来了一个问题,那就是某个对象的可及性如何判断:
◆单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
◆多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。
比如,我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可及对象。

你可能感兴趣的:(java,虚拟机,对象,内存,缓存)