深入理解Java中的四种引用类型:强引用、软引用、弱引用和虚引用(附全代码解析)

24 篇文章 0 订阅
3 篇文章 0 订阅

一、关于引用

在JDK 1.2版之前,Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表 某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了。
在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=newObject()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用

主要是为了更好的进行内存管理而设置的一套机制,粗俗的说就是不同的引用垃圾回收的力度不同。
在这里插入图片描述

二、 什么是GC

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,在使用JAVA的时候,一般不需要专门编写内存回收和垃圾清理代 码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。

电脑的内存大小的不变的,当我们使用对象的时候,如使用New关键字的时候,就会在内存中生产一个对象,但是我们在使用JAVA开发的时候,当一个对象使用完毕之后我们并没有手动的释放那个对象所占用的内存,就这样在使用程序的过程中,对象越来越多,当内存存放不了这么多对象的时候,电脑就会崩溃了,JAVA为了解决这个问题就推出了这个自动清除无用对象的功能,或者叫机制,这就是GC,有个好听是名字叫垃圾回收,其实就在用来帮你擦屁股的,好让你安心写代码,不用管内存释放,对象清理的事情了。

三、强引用(Strongly Re-ference)

强引用是最普遍的引用,如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常;只有当这个对象没有被引用时,才有可能会被回收。
强引用很常见,其写法如下:

Object obj = new Object()

可以写一个死循环来看一下

package com.mystep.step.demo1;


import java.util.ArrayList;
import java.util.List;
/**
 * @author zxj
 * @date 2021年08月15日 22:20
 */
public class StrongReferenceTest {

    static class Object {
        private Byte[] bytes = new Byte[1024 * 1024];
    }
    
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            Object obj = new Object();
            list.add(obj);
        }
    }
}

Object obj = new Object()创建的这个对象时就是强引用,上面的main方法最终将抛出OOM异常:
在这里插入图片描述

假如我们在自定义方法里面声明一个强引用的话

public void test() {
        Object strongReference = new Object();
        // 方法其他操作
    }

在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。
当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:

 obj = null;

显式地设置obj对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。

比如ArraryList类的clear方法中就是通过将引用赋值为null来实现清理工作的
在这里插入图片描述
ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null。不同于elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。

四、软引用(Soft Reference)

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。软引用也可用来实现内存敏感的高速缓存

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
在下面这个示例中我设置了JVM的内存大小,在IDEA的Run——>EditConfigiratons中设置参数:-Xms8m -Xmx8m
-XX:+PrintGCDetails
插入讲解一下设置JVM内存设置的参数:

  1. -Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;
  2. -Xms Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;
  3. -Xmn Java Heap Young区大小,不熟悉最好保留默认值;
  4. -Xss 每个线程的Stack大小,不熟悉最好保留默认值;

其中-XX:+PrintGCDetails 表示 –打印GC详细信息

下面看一下不使用ReferenceQueue

package com.mystep.step.demo1;

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

/**
 * @author step
 * @date 2021年08月15日 22:54
 * java -Xms10m -Xmx10m -XX:+PrintGCDetails
 */
public class SoftReferenceTest {

    static class StepObject {
        byte[] bs = new byte[1024 * 1024];
    }

    public static void main(String[] args) {
        SoftReference<StepObject> softReference = new SoftReference<>(new StepObject());

        List<StepObject> list = new ArrayList<>();

        while (true) {
            if (softReference.get() != null) {
                list.add(new StepObject());
                System.out.println("list.add");
            } else {
                System.out.println("---------软引用已被回收---------");
                break;
            }
            System.gc();
        }
    }
}

运行结果
在这里插入图片描述
使用ReferenceQueue引用队列

package com.mystep.step.demo1;

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

/**
 * @author step
 * @date 2021年08月15日 23:11
 * java -Xms10m -Xmx10m -XX:+PrintGCDetails
 */
public class SoftReferenceQueueTest {

    static class StepObject {
        byte[] bs = new byte[1024 * 1024];
    }

    public static void main(String[] args) {
        ReferenceQueue<StepObject> queue=new ReferenceQueue<>();
        SoftReference<StepObject> softReference = new SoftReference<>(new StepObject(),queue);

        List<StepObject> list = new ArrayList<>();

        while (true) {
            if (softReference.get() != null) {
                list.add(new StepObject());
                System.out.println("list.add");
            } else {
                System.out.println("----使用引用队列-----软引用已被回收---------");
                break;
            }
            System.gc();
        }
        Reference<? extends  StepObject> pollRef = queue.poll();
        while (pollRef != null) {
            System.out.println(pollRef);
            System.out.println(pollRef.get());
            pollRef = queue.poll();
        }

    }
}

打印结果
在这里插入图片描述
注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。
也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因。

五、弱引用(Weak Reference)

Java 中的弱引用具体指的是java.lang.ref.WeakReference<T>
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
下面我们写一个关于弱引用的场景,并在回收前和回收后打印其结果

package com.mystep.step.demo1;

/**
 * @author step
 * @date 2021年08月15日 23:44
 */
import java.lang.ref.WeakReference;

public class WeakRef {
    public static void main(String[] args) {
        WeakReference<String> srWeakRef= new WeakReference<String>(new String("我弱引用还在"));
        System.out.println(srWeakRef.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println("垃圾回收过了,看看弱引用在不在");
        System.out.println(srWeakRef.get());
    }
}

打印结果
在这里插入图片描述
垃圾回收等价于

srWeakRef= null;
System.gc();

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。
上面代码中使用这句让sr再次变为一个强引用:

String  sr = srWeakRef.get();

在这里插入图片描述
我们看到垃圾回收之后,弱引用依旧在

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
在这里插入图片描述
应用场景: 如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么应该用 Weak Reference 来记住此对象。或者想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候就应该用弱引用,这个引用不会在对象的垃圾回收判断中产生任何附加的影响。

六、 虚引用(Phantom Reference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。
虚引用与软引用和弱引用的一个区别在于虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。

package com.mystep.step.demo1;

import java.lang.ref.*;
/**
 * @author step
 * @date 2021年08月16日 0:17
 */
public class PhantomReferenceTest {
    public static void main(String[] args) throws Exception {
        // 创建一个字符串对象
        String str = new String("我是step的虚引用");
        // 创建一个引用队列
        ReferenceQueue rq = new ReferenceQueue();
        // 创建一个虚引用,让此虚引用引用到"疯狂Java讲义"字符串
        PhantomReference pr = new PhantomReference(str, rq);
        // 切断str引用和"我是step的虚引用"字符串之间的引用
        str = null;
        // 取出虚引用所引用的对象,并不能通过虚引用获取被引用的对象,所以此处输出null
        System.out.println(pr.get()); 
        // 强制垃圾回收
        System.gc();
        System.runFinalization();
        // 垃圾回收之后,虚引用将被放入引用队列中
        // 取出引用队列中最先进入队列中的引用与pr进行比较
        System.out.println(rq.poll() == pr);  
    }
}


打印结果
在这里插入图片描述

虚引用使用场景主要由两个。它允许你知道具体何时其引用的对象从内存中移除。而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后再继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。

第二点,虚引用可以避免很多析构时的问题。finalize方法可以通过创建强引用指向快被销毁的对象来让这些对象重新复活。然而,一个重写了finalize方法的对象如果想要被回收掉,需要经历两个单独的垃圾回收周期。在第一个周期中,某个对象被标记为可回收,进而才能进行析构。但是因为在析构过程中仍有微弱的可能这个对象会重新复活。这种情况下,在这个对象真实销毁之前,垃圾回收器需要再次运行。因为析构可能并不是很及时,所以在调用对象的析构之前,需要经历数量不确定的垃圾回收周期。这就意味着在真正清理掉这个对象的时候可能发生很大的延迟。这就是为什么当大部分堆被标记成垃圾时还是会出现烦人的内存溢出错误。

使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。

显而易见,finalize方法不建议被重写。因为虚引用明显地安全高效,去掉finalize方法可以虚拟机变得明显简单。当然你也可以去重写这个方法来实现更多。这完全看个人选择。

七、总结

Java4种引用的级别由高到低依次为:
强引用 > 软引用 > 弱引用 > 虚引用
在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

利用软引用和弱引用解决OOM问题:假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。

设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值