JAVA中一共有四种引用类型:强引用 Strong Reference,软引用 Soft Reference,弱引用Weak Refrence,虚引用 Phantom Reference.
接下来会一一介绍每个引用的特点与用处
强引用(Strong Reference):
强引用是我们平时写代码的时候最经常用的。例如:
public class Test{
public static void main(String[] args){
Object o = new Object();
}
}
看到上面这段代码, new Object(); 创建的实例对象就是被Object o 强引用的。
强引用的特点是什么呢?
当一个对象被强引用指向的时候,GC收集器是无法对它进行回收的,除非引用消失否则将会一直存在于堆中。
如果一直往堆中添加对象并且都是强引用,当堆中可用内存已无法接收即将要存储的对象且堆中现有对象回收不掉时,就会OOM内存溢出!
看一个下面的小例子就可以理解上面的话了:
运行下面的程序之前我对jvm启动做了一点手脚
JVM运行参数上 我加上了 -Xmx20m,这个参数的意思为设置最大堆大小为20M
public class StrongReference {
public static void main(String[] args) {
// 这里new了一个10M大小的字节数组
byte[] bytes = new byte[1024*1024*10];
System.out.println(bytes);
// 调用此方法会触发GC进行垃圾回收。可GC并不会回收掉上面的字节数组对象,因为被强引用指向的
System.gc();
// 这里我又new了一个12M大小的字节数组
byte[] bytes2 = new byte[1024*1024*12];
System.out.println(bytes2);
}
}
看了上面的小程序,是不是多少能理解强引用的特点。
这个引用是我们用的最多,也是最常见的。
软引用(Soft Reference):
软引用的写法则是 将对象放入SoftReference 类里面之后,该对象则是被软引用指向的。例如:
public class Test{
public static void main(String[] args){
SoftReference sr = new SoftReference(new byte[1024 * 1024 * 10]);
}
}
上面的程序需要分两段解读:
new SoftReference(); 创建的软引用对象被 SoftReference sr强引用的;
而 new SoftReference() 里面的new byte[1024 * 1024 * 10] 这个字节数组是被软引用的。
软引用的特点是什么呢?
软引用字面意思就可以理解为是一个软软的引用~
当堆中内存容量足够时,GC是不会回收它的,但是当堆中内存不够的时候,GC会将被软引用指向的对象回收掉!
继续看下面的一个小程序,来理解软引用的特点~
当然这次的小程序依旧对JVM参数做了手脚,-Xmx20m 设置最大堆内存为20M
public class SoftReference_0 {
public static void main(String[] args) {
// 软引用对象,里面放入了一个10m字节数组
SoftReference sr = new SoftReference(new byte[1024*1024*10]);
System.out.println("GC Before ->byte[]: "+sr.get());
System.out.println("GC Before -> sr: "+sr);
System.out.println("----------------------------");
// 执行一次GC
System.gc();
System.out.println("GC After ->byte[]: "+sr.get());
System.out.println("GC After -> sr: "+sr);
System.out.println("----------------------------");
// 创建一个12M大小bytes[]字节数组
byte[] bytes = new byte[1024 * 1024 * 12];
System.out.println("new byte[]: "+bytes);
System.out.println("new byte[] 之后再获取软引用的byte[]: "+sr.get());
System.out.println("new byte[] 之后再获取软引用的对象本身: "+sr);
}
}
解读一下上面的小程序:
1. 首先创建了一个软引用对象,里面包含了一个10m大小的byte数组
2. 通过软引用对象的get方法获取byte数组并输出,可以获取到
3. 输出软引用对象本身,也可以获取到
4. 调用了一次GC
5. 经过了一次GC后再通过软引用对象的get方法获取byte数组并输出,可以获取到
6. 经过了一次GC后再输出软引用对象本身,也可以获取到
7. 创建了一个大小为12m的字节数组
8. 输出12m大小的字节输出,可以获取到
9. 再次通过软引用对象的get方法获取byte数组并输出,此时就变成了null,获取不到了
10. 再次输出软引用对象本身,可以获取到
看完了上面的代码和日志输出,是不是会惊奇的发现这次使用软引用竟然没有出现OOM!
是的,软引用就是如此。当堆内存足够时,GC不会回收软引用指向的对象,但是当内存不够时,GC就会回收被软引用指向的对象。
可以想象成:你有一个金条,你兜里的钱够花时,你不会想着把金条送去当铺回收掉~,但是突然有一天来了一个到付快递!需要给钱,你发现你兜里没钱了 或者 钱不够花的时候,你就默默的把金条送去当铺兑换成现金,然后支付了运费,把快递拿进你家里~
软引用一般用来做什么呢?
软引用可以用来做缓存。
如果使用软引用保存数据库查询的数据,堆内存够的时候我就可以一直获取该引用指向的对象拿到我需要的数据,当堆内存不够时这些会被GC自动清理掉,如果获取引用对象null时,说明已经被清理了,那么我再去数据库查询数据就可以了!
弱引用(Weak Reference):
弱引用的写法则是 将对象放入WeakReference 类里面之后,该对象则是被弱引用指向的。例如:
public class Test{
public static void main(String[] args){
WeakReference wr = new WeakReference(new byte[1024 * 1024 * 10]);
}
}
上面的程序需要分两段解读:
new WeakReference(); 创建的软引用对象被 WeakReference wr强引用的;
而 new WeakReference() 里面的new byte[1024 * 1024 * 10] 这个字节数组是被弱引用的。
弱引用的特点是什么呢?
弱引用字面意思可以解释为是弱弱的引用,很微弱~
如果一个对象被弱引用指着,且没有其他引用所指向时,无论堆内存是否够用,只要进行GC,都将被回收。
那么什么是一个对象被弱引用指着,且没有其他引用所指向呢?
可以想象一下,当一个byte[] 被一个强引用和一个弱引用指向的时候,当遇到一次GC的时候,它会被清理吗?答案是不会被清理!因为它虽然被弱引用所指向,但它还被一个强引用所指着!!!,如果把它回收的话,那么当程序顺着强引用去堆内存找它的时候,就不能被找到了。程序就会有问题。
老样子,继续看代码进行理解~
还是老样子,依旧对JVM参数做了手脚,-Xmx20m 设置最大堆内存为20M
public class WeakReference_0 {
public static void main(String[] args) {
// 弱引用对象
WeakReference wr = new WeakReference(new byte[1024*1024*10]);
System.out.println("GC Before ->byte[]: "+wr.get());
System.out.println("GC Before -> sr: "+wr);
System.out.println("----------------------------");
System.gc();
System.out.println("GC After ->byte[]: "+wr.get());
System.out.println("GC After -> sr: "+wr);
System.out.println("----------------------------");
}
}
解读一下上面的小程序:
1. 首先创建了一个弱引用对象,里面包含了一个10m大小的byte数组
2. 通过弱引用对象的get方法获取byte数组并输出,可以获取到
3. 输出弱引用对象本身,也可以获取到
4. 调用了一次GC
5. 经过了一次GC后再通过软引用对象的get方法获取byte数组并输出,null 获取不到了
6. 经过了一次GC后再输出软引用对象本身,可以获取到
是不是就理解了弱引用的特点,只要遇到GC,就会被回收。
弱引用一般用来做什么呢?
ThreadLocal:
对没错,ThreadLocal 内部就是使用了弱引用。 具体ThreadLocal怎么用的弱引用,以及它的内存泄漏问题相关内容,请看这篇 探究-ThreadLocal内存泄露问题
虚引用(Phantom Reference):
虚引用指向的对象,我们无法通过get方法进行获取。虚引用的写法:
public class PhantomReference_0 {
public static void main(String[] args) {
PhantomReference pr =
new PhantomReference(new byte[1024 * 1024 *10],new ReferenceQueue());
}
}
上面的程序需要分两段解读:
new PhantomReference(); 创建的虚引用对象被 PhantomReference pr强引用的;
而 new PhantomReference() 里面的new byte[1024 * 1024 * 10] 这个字节数组是被弱引用的。同时还new了一个引用队列 用于记录引用对象被回收的记录。
虚引用的特点是什么呢?
虚引用是Java中最弱的引用。
它通常被用作指向堆外内存。也就是说打个比方,当GC回收到虚引用时,虚引用会告诉GC,你小心点处理,我这指向着堆外内存呢,你记得把我指向的堆外内存也处理了。以免内存泄漏。
那么它弱到什么程度呢?我们通过虚引用无法获取到被引用的对象。它指向的对象被回收后,虚引用对象本身会被加入到引用队列中,用作记录,留痕。然后当进行垃圾回收时会根据这个队列的内容 通知操作系统去处理回收掉对应的堆外内存,以防止内存泄漏。
什么叫做无法获取到虚引用指向的对象呢? 看代码进行理解~
什么?这次虚引用对象通过get方法竟然获取的是null! 此时是不是有点懵逼。此时虚引用是我们平时开发中用到的最少的,它一般用于指向堆外内存进行操作。
堆外内存是什么? 堆外内存就是同一个JVM进程,但进程内存里面不止是JVM管理的堆内存,还有堆外的内存,一般虚引用就是用来操作堆外内存的。这么说可能不太易懂,了解IO的小伙伴应该听过NIO这个东西,它为什么比普通IO快呢,就因为它可以直接将IO操作在堆外内存上。使用过NIO的话 应该知道NIO 和 Buffer配套是使用。看如下例子:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer.allocateDirect(1024); 这个静态方法,就是去堆外内存开辟一个1024个字节大小的空间,用于接下来的IO操作!来看一下源码。
点进allocateDirect()方法 发现它只是返回了一个DirectByteBuffer 对象 ,让我们继续跟进:
点击DircetByteBuffer的构造之后,你会发现它上面一通操作之后 最后调用了一个Cleaner.create的静态方法。再继续跟进就恍然大悟了:
翻了一通源码就证实了一件事,ByteBuffer的allocateDirect 会去堆外内存开辟空间去用于IO操作,普通IO是操作的JVM内部。这就是为什么NIO比普通IO快的原因。
所以~,虚引用一般有什么作用呢? 我们开发中很少用到,但是它一般用来操作堆外内存。
ps:
用于个人的记录,JAVA的四种引用类型介绍完了。如果有说的不正确的地方,还请各位大佬指正~



