美文网首页
😺Java四种引用类型:强、软、弱、虚

😺Java四种引用类型:强、软、弱、虚

作者: 我有一只喵喵 | 来源:发表于2020-12-17 23:08 被阅读0次

Java中提供了四个级别的引用:强引用、软引用、弱引用、虚引用,除强引用以外,其他的引用类型在java.lang.ref包下有具体的实现,且均派生自java.lang.ref.Reference,如图所示:

image.png

可以看到除了软弱虚引用以外,Reference还有一个派生类Finalizer,该引用类型就是用于实现我们常说的finalize函数的。

一、🍦引用队列ReferenceQueue🍦

在介绍引用类型之前,先来介绍一个与所有引用类型都相关的一个东东:引用队列java.lang.ref.ReferenceQueue。

ReferenceQueue可以和软引用、弱引用、虚引用结合使用。关于ReferenceQueue,我们只需要知道最重要的一点:ReferenceQueue中存在的引用指向的对象不是被JVM回收了,就在回收的路上

所以RerferenceQueue能干啥?既然我们知道了最重要的那一点,那么当JVM回收掉对象之后,就相当于发出了一个通知告诉我们XX被回收了,那么此时我们就可以给被回收的对象交代后事,当然交代后事这个动作也可以放在finalize()中去做,但是最好不要这么做!!

这里简单顺便提一下finalize的实现:每一个即将被回收并且包含finalize()函数的对象在正式回收前会被加入到叫做FinalizeThread线程的执行队列中,这个队列就是我们的ReferenceQueue,其中队列中的对象类型就是Finalizer,从上面的UML类图可以看出,Finalizer继承自FinalReference,每一个Finalizer包装了实际要被回收的对象,然后队列中的元素排队开始执行finalize函数,所以一个糟糕的finalize函数可能会使得对象长时间被Finalizer引用,而得不到释放,将长时间堆积在内存中,可能造成OOM,进一步增加GC压力

二、🍫强引用🍫

强引用就是程序中一般使用的引用类型,强引用的对象具有可触及不会被回收的特点。例如:

StringBuffer str = new StringBuffer("Strong Reference")

在上述代码中 str即StringBuffer实例的强引用,其中str局部变量分配在栈上,而StringBuffer实例分配在堆上(当然也可能是栈上分配),如果此时再执行如下代码:

StringBuffer str2 = str;

那么此时StringBuffer对象实例就拥有两个引用。那么怎么让某个对象实例不再拥有强引用呢?那其实就是没有任何引用指向该实例即可:

str = null;
str2 = null;

强引用具有如下特点:
1)强引用可以直接访问目标对象
2)强引用所指向的对象在任何时候都不会被系统回收,即使OOM
3)基于第二点,所以强引用可能会导致内存泄漏

三、🍧软引用🍧

软引用对应实现为java.lang.ref.SoftReference,相比较强引用稍微弱一点,假如当堆内存空间不足时,则回收软引用对象。正应如此,软引用常可以用来做缓存功能。软引用还可以结合引用队列ReferenceQueue使用,如果软引用指向的对象实例被回收,则JVM会将此软引用加入到与之关联的ReferenceQueue中

使用JVM参数-Xms7M -Xmx7m -XX:+PrintGC运行如下代码

public class SoftReferenceTest {
    public static void main(String[] args) {
        User user = new User("zhangsan");
        // 建立软引用
        SoftReference<User> userSoftReference = new SoftReference<>(user);

        // 去除user实例的强引用
        user = null;

        // 手动执行GC
        System.gc();

        // 尝试获取user对象
        if (userSoftReference.get() != null){
            System.out.println(userSoftReference.get().getUsername());
        }

        // 尝试分配4M的大对象
        byte[] bytes = new byte[1024*1024*4];

        // 再次尝试获取
        System.out.println("After OOM:"+userSoftReference.get());
    }
}

class User {
    private String username;

    private byte[] bytes = new byte[1024*1024*4];

    public User(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

运行后结果:

[GC (Allocation Failure)  1536K->819K(7680K), 0.0008617 secs]
[GC (Allocation Failure)  2355K->1300K(7680K), 0.0011494 secs]
[GC (System.gc())  6566K->5524K(7680K), 0.0026548 secs]
[Full GC (System.gc())  5524K->5258K(7680K), 0.0099074 secs]
zhangsan
[GC (Allocation Failure)  5307K->5322K(7680K), 0.0004518 secs]
[Full GC (Ergonomics)  5322K->5155K(7680K), 0.0117789 secs]
[GC (Allocation Failure)  5155K->5155K(7680K), 0.0004186 secs]
[Full GC (Allocation Failure)  5155K->1041K(7680K), 0.0093553 secs]
After OOM:null

在示例程序中,堆的大小为7M,并开启了PrintGC参数,用于发生GC时打印GC日志。接下来分为两部分:

第一部分:在main程序中首先新建了一个User对象,在User对象内部持有一个4M大小的字节数组,暂且就认为这个对象实例大小为4M(当然肯定大于4M),然后为新建的User实例建立软引用后,去除该User对象实例的强引用。此时通过System.gc()手动进行垃圾回收的触发之后,可见依然能够获取到user对象实例的内容,说明虽然发生了垃圾回收,但是其实此时内存充足,并不会回收软引用。
第二部分:紧接着第一部分,然后再次尝试分配4M大小的字节数组,由于我们的堆大小只有7M,所以此时肯定无法分配,系统将触发GC,触发GC之后,可以从回收日志看到大约回收了4M的空间(即我们的软引用对象),使得新分配的4M字节数组可以容纳。并且在这之后,软引用对象获取到的是NULL。

结论:当系统发生GC时,未必会回收软引用的对象,除非内存资源紧张不足时,软引用对象将被回收,所以软引用对象不会引起内存泄漏。

应用场景:缓存

四、🍨弱引用🍨

弱引用相比较软引用要稍微弱一点。当系统发生GC时,不管此时系统资源是否充足,都会对弱引用进行回收,当然通常情况下垃圾回收线程的优先级比较低,并不一定会及时发现持有弱引用的对象。弱引用对应的实现为java.lang.ref.WeakReference弱引用还可以结合引用队列ReferenceQueue使用,如果弱引用指向的对象实例被回收,则JVM会将此弱引用加入到与之关联的ReferenceQueue中。

使用JVM参数-Xms10M -Xmx10M -XX:+PrintGC运行如下代码:

public class WeakReferenceTask {
    public static void main(String[] args) {
        User user = new User("zhangsan");
        // 建立弱引用
        WeakReference<User> userSoftReference = new WeakReference<>(user);

        // 去除user实例的强引用
        user = null;

        // 手动执行GC
        System.gc();
        System.out.println(userSoftReference.get());

    }
}

class User {
    private String username;

    private byte[] bytes = new byte[1024 * 1024 * 4];

    public User(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

得到以下输入:

[GC (System.gc())  5747K->4784K(9728K), 0.0008500 secs]
[Full GC (System.gc())  4784K->596K(9728K), 0.0046863 secs]
null

从输出中可以看到,在手动强制进行GC之后,有明显大概4M空间的回收,且我们获取到的user是null,说明再本次GC中,我们的弱引用对象被回收了。

看完了软引用和弱引用之后,可以看到这两种引用都是比较适合做那些可有可无的缓存。当系统内存资源不足时,这些缓存数据将被回收,以提供更多的内存空间。当系统内存资源充足时,这些缓存数据又可以存在相当长的时间。

应用场景
1) ThreadLocal解决内存泄漏

先来看ThreadLocal源码中哪里用到了弱引用:
ThreadLocal.ThreadLocalMap

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

每一个线程都会有一个ThreadLocal.ThreadLocalMap,ThreadLocalMap底层其实就是一个Entry数组,Entry的key就是ThreadLocal,value就是我们要的只能被当前线程访问的对象了。注意到这个Entry继承了WeakReference,并且此弱引用表示的类型就是我们Entry的key,也就是ThreadLocal对象,首先来假设一下如果不按照上面这么写,我们可能怎么去设计Entry?

    static class Entry {
        ThreadLocal<?> key;
        Object value;
    }

我们是不是会这么设计,也能达到线程独享的目的,但是这样会有什么问题呢?再来理一理:
1、我们代码中使用了ThreadLocal达到线程独享数据

public class Test1 {
    ThreadLocal<User> threadLocal = new ThreadLocal<>();
 .....
}

2、执行Test1的Thread持有一个ThreadLocal.ThreadLocalMap
3、ThreadLocal.ThreadLocalMap持有Entry
4、Entry持有ThreadLocal和Value
5、Test1对象我们使用完了,并且也被JVM回收了,意味着我们创建的这个线程独享数据不会再使用了

上述都是强引用类型,而有些线程并不是创建完就会销毁,可能伴随着我们系统同年生同月死,那就意味着可能有些ThreadLocal以及保存的Value我们后面都不会在使用了,然后因为强引用的存在,渐渐地撑爆了我们的内存,引发内存溢出。

接下来重点就是为啥使用弱引用了
弱引用在垃圾回收时,不管资源是否充足都会回收。所以如果Entry的key是一个弱引用,那么Entry的key也就是ThreadLocal将在GC时被回收掉,那么可能有人要问了,整个Entry中key是被回收掉了,但是Value依然被强引用,依然存在啊,照样存在内存泄漏啊!

是的,所以一般只要是遇到用到了ThreadLocal的时候,一定建议或者检查是否有地方对ThreadLocal进行remove方法,显示移除此Entry

那么可能又有人要问了,既然需要我们显示remove?那还要设计key为弱引用干嘛?

我个人觉得吧,这是一个尽量解决内存泄漏的一个方案吧,因为总有粗心的程序员忘记remove对吧,或者remove永远无法被调用到等等情况,那么这个时候使用弱引用就能自动将这些不用的对象回收,并且在对ThreadLocal的get、set、remove时,如果在哈希查找的时候发现了其key是null,那么说明这个Entry失效了,此时ThreadLocal就可以保证帮我们将此Entry清理

2) WeakHashMap
其实思想和TheadLocal一样。

3)也可以用来做缓存

五、🍰虚引用🍰

虚引用是四种引用类型中最弱的一种。如果一个对象实例仅持有虚引用,那么和没有引用一样,虚引用对象随时都可能被垃圾回收器进行回收,。虚引用对应的实现为java.lang.ref.PhantomReference。软引用和弱引用的使用方式比较相似,但是虚引用相比较其他引用差别就稍微大了一点:

1)通过虚引用调用get方法获取到的永远都是null,即虚应用对象永远都是不可达的,直接看源码:

    public T get() {
        return null;
    }

2)虚引用只有一个构造方法,虚引用必须和一个引用队列ReferenceQueue结合使用

应用场景:
1) 堆外内存回收

小声哔哔:除此以外不知道这个虚引用还能有哪些场景

相关文章

网友评论

      本文标题:😺Java四种引用类型:强、软、弱、虚

      本文链接:https://www.haomeiwen.com/subject/dvoegktx.html