强引用 软引用 弱引用 虚引用 的概念剖析及测试Demo

前言:

    在分析动态代理的实现源码时,可以看到其底层使用了一个二级缓存(WeakCache类)来缓存生成的代理类的Class对象,从而了解到了Java中的弱引用WeakReference。今天我们就来系统的学习Java的四种引用方式,这里只是简要的介绍概念及通过测试Demo了解如何运用它们,关于其实现原理及JDK源码如何使用它们将以后的笔记中总结出来。

    欢迎转载:但请注明出处@_@。



目录:

  •  一:概念介绍
  1. 强引用
  2. 弱引用和软引用
  3. 虚引用
  • 二:API
  • 三:测试Demo  
  1. 设置JVM参数
  2. 强引用测试
  3. 软引用测试
  4. 弱引用测试
  5. 虚引用测试
  •  四:小结
  •  五:参考



一:概念介绍

强引用:

       我们平时用的最多的一种引用就是强引用,简单的说来一个强引用就是一个引用变量,如 StringBuilder a = new StringBuilder();这里StringBuilder类型的类型变量a就是我们new出来的那个StringBuilder()的强引用,当a一直指向new 出的那个StringBuilder对象时,该对象的空间就不会被GC掉(忽略编译器优化情况下)。

        弱引用与软引用:

        在Java语言中,当一个对象没有强可达性时(就是没有一个强引用指向该对象),JVM就有可能调用GC回收该对象所在的内存。这在大多数情况下都是好的,但是假如我有一个现在暂时不需要,但以后某个时候又很可能需要的对象的话就需要在之后再次分配了,这样一直回收分配回收分配,明显是很不划算的。因此在内存空间允许的情况下如果能将对象缓存下来就是最好不过了,但由于JVM是要对内存进行自动管理的,所以这部分不应该我们来直接管理,而是告诉虚拟机我要缓存哪个对象就好,而告诉虚拟机缓存我们想缓存哪个对象就是通过软引用和弱引用实现的(实际情况就是给对象再做一个标记,然后垃圾回收器就可以根据给出的标记做出相应的反应,决定是不是要回收该对象)。虽然软引用与弱引用都可以实现缓存,但有一个很重要的区别,将一个想要缓存的对象的引用保存在弱引用对象中后,如果在垃圾回收器看来这个对象是可以被回收的,垃圾回收器任然会直接回收该对象,完全没有给弱引用面子,所以弱引用的作用是在没有强引用指向想要缓存的对象后,在想要的缓存对象被GC前还能通过弱引用来获得缓存对象,使用该对象,并能再次用一个强引用指向它。而垃圾回收器对软引用对象指向的缓存对象态度就好很多了,它会先看看是否空间还比较充裕,如果空间充裕的话,垃圾回收器就不会在现在回收被软引用对象指向的这个对象,而是等到内存空间快不足时才回收它

       因此,在我看来,弱引用的缓存并不是真正意义上的缓存,因为它并没有阻止垃圾回收器对对象的回收,而软引用在空间充足的情况下是可以延迟对象被回收的时间的。我认为弱引用在WeakHashMap中的用法才是它存在的真正原因,我们都知道Map是通过键值对形式存储对象的,对于Map<key,value>来说如果有一个value的key被再也找不到了,也就是说我们再也拿不到key对应的那个value了,那么这个value就应该被回收,但是不幸的是,这个value只是Map的一部分,只要这个Map是活动的,我们就无法回收value对象,造成了内存泄漏。所以我们可以用WeakHashMap来保证在Map中的value不可达时能回收它,原理就是(阅读CoreJava得出的结论,还未看源码,不敢包证正确,但大致应该是这样的)WeakHashMap使用弱引用对象保存key的引用,这样当value的key消亡后,还有一个弱引用指向value,我们就可以通过弱引用来回收这个没有key的value对象了。但是在Proxy实现过程中又有一个WeakCache类,它还就真的是用弱引用来缓存的,不是很明白为何不用软引用来实现缓存。

      虚引用:

     到现在,我们已经明白了强引用可以告诉垃圾回收器不要回收其指向的对象,弱引用和软引用可以将对象缓存起来,弱引用还可以解决在Map的内存泄漏问题。在介绍虚引用的概念前还先要了解另一个类ReferenceQueue,它与SoftReference,WeakReference,和马上要介绍的PhantomReference都同属于java.lang.ref包。在使用软引用和弱引用时,我们可以将其与一个引用队列联系起来,这样的话,当弱引用和软引用对象所保存的引用指向的对象被回收的时候,这个弱引用和软引用对象将会保存到这个引用队列中。对于虚引用,我们则必须将其与一个引用队列联系起来,之所以有这个要求是因为,虚引用对象能做只是在其保存的引用的对象被回收前,让自己被放到引用队列中,这样我们就可以通过访问引用队列来看哪个对象将会被回收了,在这个对象被回收之前再做一些处理,与finalize方法的功能相似。所以虚引用的作用更像是一种通知

二:API

    强引用无特殊的API,其余的定义在java.lang.ref包中,这个包的结构为(可以的话就推荐个合适的画图工具吧):

    https://docs.oracle.com/javase/8/docs/api/

三:测试Demo

    1:设置JVM参数

   在运行程序之前我们先对JVM的堆空间的大小设置一下,并将GC日志保存到本地文件中:

    

    其中 -Xloggc表示gc日志存储位置,

           -XX:+PrintGCDetais表示打印出详细的GC信息

            -Xms:用于设置堆的最小空间 -Xmx表示堆的最大空间 我们都设置为10M


    2:强引用

package personal.johnson.reference;

import java.util.ArrayList;

public class TestReference {

    public static void main (String[] args) throws InterruptedException {


        //定义一个数组,给它分配2M内存,这个数组是拥有一个强引用b的,所以猜测即便是要发生OOM了,GC还是不会回收它
        byte[] b = new byte[2*1024*1024];


        //定义一个ArrayList<Object>对象,我们不断扩大它的空间,这样总的堆空间就会越来越少
        //我们来看看是否会为了扩list而将b指向的对象所用的空间回收(在java中,所有数组都是Object的子类)
        ArrayList<Object> list =  new ArrayList<>(0);
        //不断的增加这个数组列表的空间,直到发生OOM错误
        for(int i=1;i<1000;i++){
            if(i%100==0){
                System.out.println("目前i为"+i+":调用一次GC");
                System.gc();
                Thread.sleep(1000);
                if(b==null){
                    System.out.println("b指向的内存空间已经被回收了!");
                }else{
                    System.out.println("b指向的内存空间没有被回收");
                }
            }
            list.ensureCapacity(i*1000);
        }
    }
}

程序结果为:


打开生成的gc.log文件,内容为:    

在结果中我们可以看到:确实是发生了GC的,但是直到发生OOM错误,b指向的空间都没有没回收掉,这与我们猜想的相吻合,即便是发生OOM错误都不会回收有强引用指向的对象。因此再次论证强引用的概念:指向的对象时程序需要的,不能回收掉。


3:软引用

    将程序稍作修改,我们来看看软引用的情况是什么:

package personal.johnson.reference;

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

public class TestReference {

    public static void main (String[] args) throws InterruptedException {


        //定义一个数组,给它分配2M内存
        byte[] b = new byte[2*1024*1024];


        //定义一个引用队列
        ReferenceQueue<byte[]> refQueue = new ReferenceQueue<byte[]>();
        //定义一个软引用对象,并保存b引用
        SoftReference<byte[]> sref = new SoftReference(b,refQueue);

        //关键:在之前new byte[2*1024*1024];有两个引用指向它,一个强引用b,和我们定义的软引用sref
        //现在我们将强引用置空,看看有什么变化
        b = null;

        //定义一个ArrayList<Object>对象,我们不断扩大它的空间,这样总的堆空间就会越来越少
        //我们来看看是否会为了扩list而将b指向的对象所用的空间回收,并且可以看看回收的位置在哪里
        ArrayList<Object> list =  new ArrayList<>(0);
        //不断的增加这个数组列表的空间,直到发生OOM错误
        for(int i=1;i<1000;i++){
            if(i%100==0){
                System.out.println("目前i为"+i+":调用一次GC");
                System.gc();
                Thread.sleep(1000);
                if(sref.get()==null){
                    System.out.println("b指向的内存空间已经被回收了!");
                }else{
                    System.out.println("由于软引用的存在,b的内类空间还没有被回收");
                }
                System.out.println(refQueue.poll()==null?"refQueue为空":"refQueue不为空");
            }
            list.ensureCapacity(i*1000);
        }
    }
}

程序的运行结果为:

    

从程序中可以看到,当我们将变量b置为空之后,按道理来说,应该回收了它指向的内存空间,但由于我们又用一个软引用指向了这个空间,所以,当内存充裕的时候并没有回收,而是等到空间不足的时候才回收了这片内存。同时只有当软引用对象保存的引用指向的空间被GC后,软引用对象才放置到引用队列中。gc.log在这个地方就不需要拿出来了。

4:弱引用

package personal.johnson.reference;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

public class TestReference {

    public static void main (String[] args) throws InterruptedException {


        //定义一个数组,给它分配2M内存
        byte[] b = new byte[2*1024*1024];


        //定义一个引用队列
        ReferenceQueue<byte[]> refQueue = new ReferenceQueue<byte[]>();
        //定义一个弱引用对象,并保存b引用
        WeakReference<byte[]> wref = new WeakReference<byte[]>(b,refQueue);

        //关键:在之前new byte[2*1024*1024];有两个引用指向它,一个强引用b,和我们定义的弱引用wref
        //现在我们将强引用置空,看看有什么变化
        b = null;


        //先看是否能从弱引用获得对象的引用
        if(wref.get()!=null){
            System.out.println("还可以通过弱引用访问b指向的对象的空间\n\n");
        }

        //定义一个ArrayList<Object>对象,我们不断扩大它的空间,这样总的堆空间就会越来越少
        //我们来看看是否会为了扩list而将b指向的对象所用的空间回收,并且可以看看回收的位置在哪里
        ArrayList<Object> list =  new ArrayList<>(0);
        //不断的增加这个数组列表的空间,直到发生OOM错误
        for(int i=1;i<1000;i++){
            if(i%100==0){
                System.out.println("目前i为"+i+":调用一次GC");
                System.gc();
                Thread.sleep(1000);
                if(wref.get()==null){
                    System.out.println("b指向的内存空间已经被回收了,因为弱引用并不能延迟对该对象的GC!");
                    System.out.println(refQueue.poll()==null?"refQueue为空":"refQueue不为空");
                }
            }
            list.ensureCapacity(i*1000);
        }
    }
}

结果为:


从结果中,可以看到用弱引用保存对象的引用后,当对象的强引用设置为null后,还是可以能够通过弱引用去访问它的,但是从结果中,我们可以明显的看到弱引用与软引用的不同,它并没有延长对象的生命周期,在第一次指向GC的时候就回收了该空间,并将弱引用放入到引用队列中。这就是为什么我之前觉得弱引用不适合做缓存,关于这点,欢迎讨论哦^_^;

5.虚引用:

package personal.johnson.reference;

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

public class TestReference {

    public static void main (String[] args) throws InterruptedException {


        //定义一个数组,给它分配2M内存
        byte[] b = new byte[2*1024*1024];
        b[0] = 21;


        //定义一个引用队列
        ReferenceQueue<byte[]> refQueue = new ReferenceQueue<byte[]>();
        //定义一个虚引用对象,并保存b引用
        PhantomReference<byte[]> pref = new PhantomReference<byte[]>(b,refQueue);

        //关键:在之前new byte[2*1024*1024];有两个引用指向它,一个强引用b,和我们定义的虚引用pref
        //现在我们将强引用置空,看看有什么变化
        b = null;


        //先看在无强引用的情况下是否能从虚引用获得对象的引用
        //注:在软引用和弱引用中是可以获得的
        if(pref.get()!=null){
            System.out.println("还可以通过虚引用访问b指向的对象的空间\n\n");
        }else{
            System.out.println("无法通过虚引用访问b指向的对象的空间\n\n");
        }

        //定义一个ArrayList<Object>对象,我们不断扩大它的空间,这样总的堆空间就会越来越少
        //我们来看看是否会为了扩list而将b指向的对象所用的空间回收,并且可以看看回收的位置在哪里
        ArrayList<Object> list =  new ArrayList<>(0);
        //不断的增加这个数组列表的空间,直到发生OOM错误
        for(int i=1;i<500;i++){
            if(i%100==0){
                System.out.println("目前i为"+i+":调用一次GC");
                System.gc();
                Thread.sleep(1000);
                if(pref.get()==null){
                    System.out.println("b指向的内存空间已经被回收了,因为虚引用并不能延迟对该对象的GC!");
                }
                //注意,此时pRef所保存的引用指向的空间,并没有被GC回收,在我们显式地调用refQueue.poll返回pRef之后
                //当GC第二次发现虚引用,而此时JVM将pRef插入到refQueue会插入失败,此时GC才会对obj进行回收
                //注:本段注释来自:https://blog.csdn.net/aitangyong/article/details/39453365
                if(refQueue.poll()!=null) {
                    System.out.println("由于回收了虚引用保存的引用指向的内存空间,所以将虚引用放入到了引用列表中,我们可以因此来做一些事情" +
                            "比如现在,我们知道虚引用保存的引用指向的内存空间已经挂了");
                }
                //当这个队列不为空后,我们知道pRef保存的引用指向的空间已经被回收了,我们可以因此来做一些事情
            }
            list.ensureCapacity(i*1000);
        }
    }
}

结果为:


从结果中可以看到,在对象的强引用丢失后并不能通过虚引用访问到对象(在对象还有强引用的时候是可以的)。因此,说虚引用更像一种通知,在这里我们就打印了一条消息,说虚引用保存的引用指向的空间已经被回收了。注意,虚引用保存的引用指向的对象在调用虚引用关联的引用队列的poll方法之前都是没有被回收的,直到调用引用队列的poll方法之后,虚拟机再次检测到这个弱引用时才会回收对象的空间。


四:小结

 将从这四种引用的作用,与何时回收它关联的内存空间来看:

     强引用:用于访问对象,告知JVM不要清理引用的对象,只要对象有强引用就永不清理         

     软引用:用于缓存对象,在空间充裕的时候不回收软引保存的引用指向的对象的空间,空间不足时才回收,在对象被回收之前(可能GC已经运行多次了)都可以通过它访问到对象

     弱引用:用于缓存和与垃圾回收器配合清理map中的不可达对象,对象的内存在没有强引用后第一次GC算法调用的时候清理,与内存是否充裕无关,但在GC调用之前还可以通过它访问到对象

     虚引用:只有通知作用,如果只有虚引用,是无法访问到它关联的对象的,在调用这个虚引用关联的引用队列的poll方法之后,垃圾回收器再次发现这个虚引用的时候回收它关联的内存,。


五:参考

java中的4种reference的差别和使用场景(含理论、代码和执行结果):

    https://blog.csdn.net/aitangyong/article/details/39453365

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值