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

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) 堆外内存回收

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容