Java 强引用 软引用 弱引用 虚引用


版权声明:本文为CSDN博主「Zs_Johnson」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_15802379/article/details/80898636

目录:

  •  一:概念介绍
  1. 强引用
  2. 弱引用和软引用
  3. 虚引用

1、强引用

     强引用不会被GC回收,并且在java.lang.ref里也没有实际的对应类型,平时工作接触的最多的就是强引用。
  Object obj = new Object();这里的obj引用便是一个强引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2、软引用

如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只 要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
————————————————
版权声明:本文为CSDN博主「aitangyong」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/aitangyong/article/details/39453365

Object obj = new Object();
    ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
    SoftReference<Object> softRef = new SoftReference<Object>(obj, refQueue);

    System.out.println(softRef.get()); // [email protected]
    System.out.println(refQueue.poll());// null
 

3、弱引用

如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。  弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回 收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 

  1. /**

  2. * 弱引用: 当发生GC的时候,Weak引用对象总是会内回收回收。因此Weak引用对象会更容易、更快被GC回收。

  3. * Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象

   Object obj = new Object();
    ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
    WeakReference<Object> weakRef = new WeakReference<Object>(obj, refQueue);

    System.out.println(weakRef.get()); // [email protected]
    System.out.println(refQueue.poll());// null
 

4、幽灵引用(虚引用)

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。由于Object.finalize()方法的不安全性、低效性,常常使用虚引用完成对象回收前的资源释放工作。参考我的另一篇博客:解释为什么finalize是不安全的,不建议使用
 

  1. /**

  2. * 当GC一但发现了虚引用对象,将会将PhantomReference对象插入ReferenceQueue队列.

  3. * 而此时PhantomReference所指向的对象并没有被GC回收,而是要等到ReferenceQueue被你真正的处理后才会被回收.

Object obj = new Object();
    ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
    PhantomReference<Object> phantom = new PhantomReference<Object>(obj,
            refQueue);

    System.out.println(phantom.get()); // [email protected]
    System.out.println(refQueue.poll());// null


 obj = null;
    System.gc();
 
    // 调用phanRef.get()不管在什么情况下会一直返回null
    System.out.println(phantom.get());
 
    // 当GC发现了虚引用,GC会将phanRef插入进我们之前创建时传入的refQueue队列
    // 注意,此时phanRef所引用的obj对象,并没有被GC回收,在我们显式地调用refQueue.poll返回phanRef之后
    // 当GC第二次发现虚引用,而此时JVM将phanRef插入到refQueue会插入失败,此时GC才会对obj进行回收
    Thread.sleep(200);
    System.out.println(refQueue.poll());

这里特别需要注意:当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。如NIO直接内存的自动回收,就使用到了sun.misc.Cleaner

public class Cleaner extends PhantomReference
{

 

JDK底层源码查询网站:http://www.docjar.com/html/api/sun/misc/Cleaner.java.html

5、小结

引用和引用队列提供了一种通知机制,允许我们知道对象已经被销毁或者即将被销毁。GC要回收一个对象的时候,如果发现该对象有软、弱、虚引用的时候,会将这些引用加入到注册的引用队列中。软引用和弱引用差别不大,JVM都是先把SoftReference和WeakReference中的referent字段值设置成null,之后加入到引用队列;而虚引用则不同,如果某个堆中的对象,只有虚引用,那么JVM会将PhantomReference加入到引用队列中,JVM不会自动将referent字段值设置成null。

参考资料:
Java:对象的强、软、弱和虚引用    http://zhangjunhd.blog.51cto.com/113473/53092/

Java 中的 Reference              http://www.cnblogs.com/newcj/archive/2011/05/15/2046882.html
 

 

一:概念介绍

强引用:

       我们平时用的最多的一种引用就是强引用,简单的说来一个强引用就是一个引用变量,如 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日志保存到本地文件中:

    Java 强引用 软引用 弱引用 虚引用

    其中 -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);
        }
    }
}
程序结果为:

Java 强引用 软引用 弱引用 虚引用

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

Java 强引用 软引用 弱引用 虚引用

在结果中我们可以看到:确实是发生了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);
        }
    }
}
程序的运行结果为:

    Java 强引用 软引用 弱引用 虚引用

从程序中可以看到,当我们将变量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);
        }
    }
}
结果为:

Java 强引用 软引用 弱引用 虚引用

从结果中,可以看到用弱引用保存对象的引用后,当对象的强引用设置为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);
        }
    }
}
结果为:

Java 强引用 软引用 弱引用 虚引用

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

四:小结

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

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

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

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

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

五:参考

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

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


版权声明:本文为CSDN博主「Zs_Johnson」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_15802379/article/details/80898636