专栏首页BAT的乌托邦聊聊Java的引用类型(强引用、软引用、弱引用、虚引用),示例WeakHashMap的使用【享学Java】

聊聊Java的引用类型(强引用、软引用、弱引用、虚引用),示例WeakHashMap的使用【享学Java】

前言

Java语言中的数据类型可划分为值类型引用类型。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。 这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用

值类型和引用类型

  • 值类型:byte、short、int、long、float、double、char、boolean
  • 引用类型:除了值类型,所有的类型都是引用类型。(数组、字符串、类、接口等)

一个具有值类型的数据存放在栈内的一个变量中:栈内分配内存空间,直接存储所包含的值,其值代表的数据本身。 而引用类型数据的变量值会存放在堆中变量名(引用地址)会存放在栈中

值类型的数据具有较快的存取速度

Java引用类型

在Java中提供了四个级别的引用:强引用,软引用,弱引用和虚引用。

// @since    1.2
public abstract class Reference<T> {

	// 构造函数是非public的  所以只能通包内的类才能访问此构造函数
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
	...
    public T get() {
        return this.referent;
    }
    public void clear() {
        this.referent = null;
    }
    public boolean isEnqueued() {
        return (this.queue == ReferenceQueue.ENQUEUED);
    }
    public boolean enqueue() {
        return this.queue.enqueue(this);
    }
	...
}

Reference<T>是JDK1.2提供的引用类型,它有四个子类分别代表着四种引用类型。其中只有强引用FinalReference类是包内可见,其余的都是public的。这也侧面表示了:强引用类型是Java默认的引用类型,直接使用即可

这四个引用定义在java.lang.ref的包下

强引用

强引用( Final Reference):只要强引用还存在,垃圾收集器永远不会回收(JVM宁愿抛出OOM异常也不回收强引用所指向的对)被引用的对象。但是,强引用可能会造成内存泄露哦~

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person("fsx", 18);
        System.out.println(person); //强引用:直接使用
    }

}

这就是强引用的典型示例。

强引用的特点:

  1. 可以直接访问目标对象。
  2. 所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。
  3. 可能导致内存泄漏。

若你的内存够大,无需考虑极致性能,所有引用都使用强引用也是ok的~ 强引用可直接访问,是Java默认的引用方式~



注意:我看到很多文章把FinalReference解释为强引用,这是错误的。

/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

从它的javadoc也能看出来,这个类它和强引用没半毛钱关系。它倒是和Object的finalize()方法有关。关于它的详细介绍,可参阅笨神的大作:JVM源码分析之FinalReference完全解读

关于finalize方法的使用,亦可参阅这篇文章:java finalize方法总结、GC执行finalize的过程



软引用

软引用(Soft Reference):是用来描述一些还有用但并非必须的对象。对于软引用对象,如果内存充足(注意此处对充足二字的理解)gc不会管它,如果内存不够了,它就不能幸免了

public class SoftReference<T> extends Reference<T> {
    static private long clock;
    private long timestamp;

	// 提供了两个构造函数 可以构造软引用
    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }
    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

	// 获取此软引用的对象
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

软引用是除了强引用外最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。

SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。 也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前SoftReference类所提供的get()方法返回Java对象的强引用。一旦垃圾线程回收该Java对象之后,get()方法将返回null。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Person obj = new Person("fsx", 18);
        SoftReference sf = new SoftReference<>(obj);
        obj = null; //置为null 让obj被垃圾回收期回收

        //byte[] bytes = new byte[1024 * 100];
        //System.gc();
        //TimeUnit.SECONDS.sleep(1); // 把问题放大,让gc过来回收 保证obj已经被回收

        System.out.println("是否被回收:" + sf.get());
    }

}

此处注意:当obj=null后,这时,Obj对象就只剩下软引用了,是软可达的。所以哪怕这个时候调用System.gc();也是不会被回收的

运行时请调整虚拟机参数为:-Xmx2m -Xms2m规定堆内存大小为2m。

运行结果:

是否被回收:Person{name='fsx', age=18}

打开被注释掉new byte[1024*100]语句,这条语句请求一块大的堆空间,使堆内存使用紧张。并显式的再调用一次GC,结果如下:

是否被回收:null

obj对象已经被回收,软可达已经变成了不可达

此处不知你是否还能看出一个问题: obj被gc回收后,SoftReference sf这个对象就成垃圾了,完全没用了,这个时候建议处理掉,否则可能造成内存泄漏现象。所以ReferenceQueue队列上场啦。

ReferenceQueue队列和JVM对象垃圾回收机制有关,垃圾回收器将已注册的引用对象添加到队列中,ReferenceQueue实现了入队(enqueue)和出队(poll),还有remove操作,内部元素head就是泛型的Reference

软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中

上例中SoftReference sf对象的内存回收的时候会经历一个过程:从Active->Pending->Enqueued->Inactivepending状态就是等待着进入ReferenceQueue队列的这样一个状态,说白了它目前还没被回收,只是对象的引用(此处的obj对象)被移除了pending保存了这个引用,并且放进ReferenceQueue里(更详细的可咨询JVM的对象回收流程)。

这段话建议多读几遍,好好理解一番。 引用对象指向的对象 GC 会自动清理,但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。

so如下示例代码手动清理引用本身:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        //定义一个引用队列
        ReferenceQueue<Person> queue = new ReferenceQueue<>();

        Person obj = new Person("fsx", 18);
        SoftReference sf = new SoftReference<>(obj, queue); // 通过构造函数把Queue传进去 让此软引用关联上队列
        obj = null; //置为null 让obj被垃圾回收期回收

        System.gc();
        TimeUnit.SECONDS.sleep(2);

        byte[] bytes = new byte[1024 * 100];
        System.gc();
        TimeUnit.SECONDS.sleep(1); // 把问题放大,让gc过来回收 保证obj已经被回收

        System.out.println("是否被回收:" + sf.get());
        //队列里存在 说明对象马上就要被回收了 所以顺势也把软引用对象干掉
        if ((sf = (SoftReference<Person>) queue.poll()) != null) {
            System.out.println("sf置为null,释放内存");
            sf = null;
        }
    }

}

输出结果:

是否被回收:null
sf置为null,释放内存

可见这么操作后,sf也被释放了。注意:若没有这句话new byte[1024 * 100],内部不会紧张,obj也不会被回收,所以sf置为null,释放内存这句话也就不会被打印了

弱引用

弱引用(WeakReference):弱引用和软引用很像,当gc时,无论内存是否充足,都会回收被弱引用关联的对象。 它也可以和ReferenceQueue配合使用:如果弱引用所引用的对象被JVM回收,这个弱引用就会被加入到与之关联的引用队列中(使用方式同上示例)

public class WeakReference<T> extends Reference<T> {
	
	// 仅仅提供了两个构造方法而已,注意它的get方法在父类Reference上
    public WeakReference(T referent) {
        super(referent);
    }
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

对它多一个使用Demo如下:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Person obj = new Person("fsx", 18);
        WeakReference sf = new WeakReference(obj);
        obj = null;
        System.out.println("是否被回收" + sf.get());
        System.gc();
        System.out.println("是否被回收" + sf.get());
    }

}

输出:

是否被回收Person{name='fsx', age=18}
是否被回收null

可见即使内存是足够的,但只要GC了,它也会被回收。注意此时WeakReference sf它还在,为了避免成为垃圾,建议按照上面做处理。


说明:软引用,弱引用都非常适合来保存那些可有可无的缓存数据,如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。


虚引用

虚引用(PhantomReference):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期java对象的生命周期)。 一个对象与虚引用关联,则跟没有引用与之关联一样,所以get()方法永远返回null,在任何时候都可能被垃圾回收器回收。因此它必须ReferenceQueue一起使用,否则没有任何意义它的作用在于跟踪垃圾回收过程

public class PhantomReference<T> extends Reference<T> {

	// 它永远返回null
	@Override
    public T get() {
        return null;
    }

	// 它只有这个一个构造函数:必须和ReferenceQueue一起使用~
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收销毁这个对象之前,将这个虚引用加入引用队列。 因此我们可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动(比如释放资源等等操作)

public class Main {

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Person> queue = new ReferenceQueue<>();
        Person obj = new Person("fsx", 18);

        PhantomReference sf = new PhantomReference<>(obj, queue);
        obj = null;

        System.out.println(sf.get());
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        if ((sf = (PhantomReference<Person>) queue.poll()) != null) {
            System.out.println("obj要销毁了,准备释放内存");
            sf = null;
        }
    }

}

运行结果:

null
obj要销毁了,准备释放内存

使用场景(WeakHashMap

JDK1.2既然推出了这四种引用类型,那么势必是有它的使用场景的。

  1. 使用软引用构建敏感数据的缓存(如用户的基本信息,毕竟用户信息基本不变但经常用到)
  2. 使用弱引用构建非敏感数据的缓存。如JDK内置的WeakHashMap当一个键对象被垃圾回收器回收时,那么相应的值对象的引用会从WeakHashMap中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据

我们平时经常使用Map来缓存数据,其实这样很多时候会造成大量的内存泄漏。下面用示例来证明这一点:

同样的,运行时请调整虚拟机参数为:-Xmx2m -Xms2m规定堆内存大小为2m。

    public static void main(String[] args) throws InterruptedException {
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < 10000; i++) {
            map.put("key" + i, new byte[i]);
        }
        System.out.println(map.size());
    }

结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.fsx.maintest.Main.main(Main.java:16)

因为我们用的HashMap,所以最终内存不够就OOM了~

使用WeakHashMap改进:

WeakHashMap类在java.util包内,它实现了Map接口,是HashMap的一种实现,它使用弱引用作为内部数据的存储方案WeakHashMap是弱引用的一种典型应用它可以作为简单的缓存表解决方案

// @since 1.2  JDK1.2出来的
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
	// 内部自己new了一个ReferenceQueue
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
	...
	// 它使用若引用的核心主要是Entry这个内类
	// 继承自WeakReference,这样一来,整个Entry就是一个WeakReferenc
    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        Entry(Object key, V value,ReferenceQueue<Object> queue,int hash, Entry<K,V> next) {
            super(key, queue); // 调用WeakReference的构造函数
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
    }
	...
}

使用WeakHashMap改造如下(只改一行代码):

public class Main {

    public static void main(String[] args) throws InterruptedException {
        // 只改变了这一句话而已~
        Map<String, Object> map = new WeakHashMap<>();
        for (int i = 0; i < 10000; i++) {
            map.put("key" + i, new byte[i]);
        }
        System.out.println(map.size());
    }

}

输出:

6(备注:这个值可能是10、20、100等等)

从结果中可以看出Map的size()长度不是10000了,也就是说出现了数据丢失~,但运行不会报OOM错误了。 由此可见,WeakHashMap会在系统内存紧张时使用弱引用,自动释放掉持有弱引用的内存数据。

说明:如果WeakHashMap的key都在系统内持有强引用,那么WeakHashMap就退化为普通的HashMap,因为所有的表项都无法被自动清理。 所以尽量不要使你的WeakHashMap里的key被强引用了,否则它将失去效果~~~

关于WeakHashMap的深层使用原理,本文不做讨论。

当然我们也可以自定义一个基于WeakReference或者SoftReference的缓存结构,有兴趣自定义的可以参考这个示例:java中SoftReference与WeakReference应用于高速缓存示例

总结

其实软引用、弱引用在Android这种移动端应用,对内存更敏感的应用中使用更多些,服务端毕竟对内存敏感度差点,所以没收到很多人的重视。但是这一块个人觉得在设计高效的框架时,还是可以使用的~ Tips:谷歌不推荐使用软引用SoftReference,而建议使用若引用WeakReference

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【小家java】引用类型(强引用、软引用、弱引用、虚引用)

    本文不论述java中值传递和引用传递之间的问题(有需求的可移步理解java中值传递和引用传递),而重点讨论Java中提供了4个级别的引用:强应用、软引用、弱引用...

    YourBatman
  • [享学Netflix] 四、Apache Commons Configuration2.x定位FileLocator和FileHandler

    上一篇讲述了Commons Configuration2.x它全新的事件-监听基础,一方面体会到了相较于1.x的改动之大,另一方面也能感受到2.x在可扩展性方面...

    YourBatman
  • 【小家Java】聊聊Java中的比较器(排序):Comparable和Comparator;Spring中的Comparators和AnnotationAwareOrderComparator

    “顺序“在任何一个领域里都是非常重要的一个概念,程序也不例外。不同的执行顺序,能对你的执行结果产生直接影响。

    YourBatman
  • 【小家java】引用类型(强引用、软引用、弱引用、虚引用)

    本文不论述java中值传递和引用传递之间的问题(有需求的可移步理解java中值传递和引用传递),而重点讨论Java中提供了4个级别的引用:强应用、软引用、弱引用...

    YourBatman
  • 详解 Java 中的四种引用

    在 Java 中,引用随处可见,我们通过类似 Object obj = new Object(); 的代码就可以创建一个引用,而我们直接通过这个代码段创建的引用...

    指点
  • MVC中的引用缺少问题

    有的时候引用就会少了上面者2个部分,然后后面你执行什么内容都会出错,这时候就要把这2个引用引进来,首先右键点击添加引用然后到下一个页面点击浏览,然后就找到你这个...

    PHY_68
  • 强引用,软引用,弱引用,幻象引用有什么区别?

    不同的引用类型,主要体现的是对象的不同的可达性(reachable)状态和对垃圾收集的影响。

    王小明_HIT
  • 理解Java中的强引用,软引用,弱引用,虚引用

    在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在商店购买了某样物...

    IT大咖说
  • 三、对象已死?

    在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”

    大数据程序猿
  • 你知道Java的四种引用类型吗?

    在Java中提供了四个级别的引用:强引用,软引用,弱引用和虚引用。在这四个引用类型中,只有强引用FinalReference类是包内可见,其他三种引用类型均为p...

    Java技术江湖

扫码关注云+社区

领取腾讯云代金券