Java中提供了四个级别的引用:强引用、软引用、弱引用、虚引用,除强引用以外,其他的引用类型在java.lang.ref包下有具体的实现,且均派生自java.lang.ref.Reference,如图所示:
可以看到除了软弱虚引用以外,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) 堆外内存回收
小声哔哔:除此以外不知道这个虚引用还能有哪些场景