转载 原文地址
以前学习强软弱虚引用的时候,只是走马观花看看博客,并没有自己写代码去实践、去证明,导致每次看完后,过不了多久就忘了,后来下定决心,一定要自己敲敲代码,这样才能让印象更加深刻,古人云:纸上得来终觉浅,绝知此事要躬行。
Java中的四种引用
Java中有四种引用类型:强引用、软引用、弱引用、虚引用。
Java为什么要设计这四种引用
Java的内存分配和内存回收,都不需要程序员负责,都是由伟大的JVM去负责,一个对象是否可以被回收,主要看是否有引用指向此对象,说的专业点,叫可达性分析。
Java设计这四种引用的主要目的有两个:
- 可以让程序员通过代码的方式来决定某个对象的生命周期;
- 有利用垃圾回收。
强引用
强引用是最普遍的一种引用,我们写的代码,99.9999%都是强引用:
1 | Object o = new Object(); |
这种就是强引用了,是不是在代码中随处可见,最亲切。
只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。
那么什么时候才可以被回收呢?当强引用和对象之间的关联被中断了,就可以被回收了。
我们可以手动把关联给中断了,方法也特别简单:
1 | o = null; |
我们可以手动调用GC,看看如果强引用和对象之间的关联被中断了,资源会不会被回收,为了更方便、更清楚的观察到回收的情况,我们需要新写一个类,然后重写finalize方法,下面我们来进行这个实验:
1 | public class Student { |
1 | public static void main(String[] args) { |
运行结果:
1 | Student 被回收了 |
可以很清楚的看到资源被回收了。
当然,在实际开发中,千万不要重写finalize方法
在实际的开发中,看到有一些对象被手动赋值为NULL,很大可能就是为了“特意提醒”JVM这块资源可以进行垃圾回收了。
软引用
软引用用来描述一些还有用,但非必须的对象.下面先来看看如何创建一个软引用:
1 | SoftReference<Student> studentSoftReference = new SoftReference<Student>(new Student()); |
软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了:
1 | SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student()); |
软引用有什么特点呢:当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象。
还是一样的,必须做实验,才能加深印象:
1 | SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*10]); |
我定义了一个软引用对象,里面包裹了byte[],byte[]占用了10M,然后又创建了10Mbyte[]。
运行程序,需要带上一个参数:
1 | -Xmx20M |
代表最大堆内存是20M。
运行结果:
1 | [B@11d7fff |
可以很清楚的看到手动完成GC后,软引用对象包裹的byte[]还活的好好的,但是当我们创建了一个10M的byte[]后,最大堆内存不够了,所以把软引用对象包裹的byte[]给干掉了,如果不干掉,就会抛出OOM。
软引用到底有什么用呢?比较适合用作缓存,当内存足够,可以正常的拿到缓存,当内存不够,就会先干掉缓存,不至于马上抛出OOM。比如Mybatis的二级缓存回收策略为Soft时,其实现类SoftCache
就是一个软引用类型.
弱引用
弱引用和软引用类似,都是用来描述非必须对象的,但它只能生存到下一次GC前,关键字为WeakReference:
1 | WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024*1024*10]); |
根据JDK文档,弱引用常用来实现规范化映射,特点是不管内存是否足够,只要发生GC,都会被回收:
1 | WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1]); |
运行结果:
1 | [B@11d7fff |
可以很清楚的看到明明内存还很充足,但是触发了GC,资源还是被回收了。弱引用在很多地方都有用到,比如ThreadLocal
、WeakHashMap
。
ThreadLocal
通过使用弱引用Entry
来避免内存泄漏,我们知道ThreadLocal
变量在不同的线程中有不同的值,原理是每个线程都有一个ThreadLocalMap
,用来存放ThreadLocal
变量表.ThreadLocal
本身并不存储值,它只是作为一个 key
来让线程从 ThreadLocalMap
获取 value
。结构如下:
图中的虚线表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key
的,当ThreadLocalRef
使用完毕释放以后,仅有弱引用的对象ThreadLocal
在 GC 时会被回收。
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
这里为什么要使用弱引用呢?
如果不使用弱引用,当持有value的强引用释放掉后,而且线程没有结束(回收释放)时,threadLocalMap
会一直持有ThreadLocal以及value的强引用,导致value不能够被回收,从而造成内存泄漏。
通过使用弱引用,当ThreadLocal的强引用释放掉后,通过一次系统gc检查,发现ThreadLocal对象只有threadLocalMap中Entry的若引用持有,此时根据弱引用的机制就会回收ThreadLocal对象,从而避免了内存泄露。
虚引用
虚引用又被称为幻影引用,我们来看看它的使用:
1 | ReferenceQueue queue = new ReferenceQueue(); |
虚引用的使用和上面说的软引用、弱引用的区别还是挺大的,我们先不管ReferenceQueue 是个什么鬼,直接来运行:
1 | null |
竟然打印出了null,我们来看看get方法的源码:
1 | public T get() { |
这是几个意思,竟然直接返回了null。
这就是虚引用特点之一了:无法通过虚引用来获取对一个对象的真实引用。
那虚引用存在的意义是什么呢?这就要回到我们上面的代码了,我们把代码复制下,以免大家再次往上翻:
1 | ReferenceQueue queue = new ReferenceQueue(); |
创建虚引用对象,我们除了把包裹的对象传了进去,还传了一个ReferenceQueue
,从名字就可以看出它是一个队列。
虚引用的特点之二就是 虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
我们来用代码实践下吧:
1 | ReferenceQueue queue = new ReferenceQueue(); |
运行结果:
1 | Student 被回收了 |
我们简单的分析下代码:
第一个线程往集合里面塞数据,随着数据越来越多,肯定会发生GC。
第二个线程死循环,从queue里面拿数据,如果拿出来的数据不是null,就打印出来。
从运行结果可以看到:当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。
虚引用有什么用呢?在NIO中,就运用了虚引用管理堆外内存。