在Java中提供了4个级别的引用:强引用,软引用,弱引用,虚引用。在这4个引用级别中,只有强引用FinalReference类是包内可见,其他3中引用类型均为​​public​​,可以在应用程序中直接使用。

强引用

Java中的引用,有点像C++的指针,通过引用,可以对堆中的对象进行操作。
在我们的代码生涯中,大部分使用的都是强引用,所谓强引入,都是形如​​​Object o = new Object()​​​的操作。
强引用具备一下特点:

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候不会被系统回收,JVM宁愿抛出OOM异常,也不回收强引用所指向的对象
  • 强引用可能导致内存泄漏

所以当我们在使用强引用创建对象时,如果下面不使用这个对象了,一定要显式地使用​​o = null​​​操作来辅助垃圾回收器进行gc操作。一旦我们没有进行置​​null​​​操作,就会造成内存泄漏,再使用MAT等工具进行复杂操作,浪费了大量的时间。
在jdk代码中也有许多显式置​​​null​​​的操作。对于​​ArrayList​​​,相信了解过java的都知道,​​ArrayList​​​底层使用的数组实现的,在我们进行​​clear​​操作时,就会对数组进行置null操作。

public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

其实如果我们的对象​​Object o = new Object()​​​是在方法内创建的,那么局部变量​​o​​​将被分配在栈上,而对象​​Object​​​实例被分配在堆上,局部变量​​o​​​指向​​Object​​​实例所在的对空间,通过​​o​​​可以操作该实例,那么​​o​​​就是​​Object​​​的引用。这时候显式置​​null​​​的作用不大,只要在我们的方法退出,即该栈桢从Java虚拟机栈弹出时,​​o​​​指向​​Object​​​的引用就断开了,此时​​Object​​在堆上分配的内存在GC时就能被回收。

软引用

软引用是除强引用外,最强的引用类型。可以通过​​java.lang.ref.SoftReference​​​使用软引用,一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收,当堆使用率临近阈值时,才会去回收软引用对象。只要有足够的内存,软引用便可能在内存中存活相当长一段时间。因此,软引用可以用于实现对内存敏感的Cache。
在java doc中,软引用是这样描述的

虚拟机在抛出 OutOfMemoryError 之前会保证所有的软引用对象已被清除。此外,没有任何约束保证软引用将在某个特定的时间点被清除,或者确定一组不同的软引用对象被清除的顺序。不过,虚拟机的具体实现会倾向于不清除最近创建或最近使用过的软引用。

举个例子

/**
*
* @author xiaosuda
* @date 2018/10/23
*/
public class ReferenceTest {


public static void main(String[] args) {

//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
//创建软引用
SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
//检查引用队列,监控对象回收情况
new Thread(new CheckRefQueue(queue)).start();
//删除强引用
object = null;
//手动GC
System.gc();
System.out.println("After GC:Soft Get= " + softRef.get());
System.out.println("分配大内存:" + Runtime.getRuntime().maxMemory());
try {
//分配大内容
byte[] maxObject = new byte[(int) Runtime.getRuntime().maxMemory()];
} catch (Throwable e) {
System.out.println(e.getMessage());
}
System.out.println("After new byte[]:Soft Get= " + softRef.get());
}

}

class CheckRefQueue implements Runnable {

ReferenceQueue queue;

public CheckRefQueue(ReferenceQueue queue) {
this.queue = queue;
}

@Override
public void run() {
Reference myObj = null;
try {
myObj = queue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (myObj != null) {
System.out.println("Object for SoftReference is " + myObj.get());
}
}
}

class MyObject {


@Override
protected void finalize() throws Throwable {
super.finalize();
//被回收时输出
System.out.println("MyObject's finalize called");
}

@Override
public String toString() {
return "I'am MyObject";
}
}

JVM参数为:-Xmx5m -XX:+PrintGCDetails -Xmn2m
执行结果如下:

[GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0015474 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->425K(4096K)] 520K->425K(5632K), [Metaspace: 2687K->2687K(1056768K)], 0.0050401 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
After GC:Soft Get= I'am MyObject
分配大内存:5767168
[GC (Allocation Failure) [PSYoungGen: 20K->64K(1536K)] 445K->489K(5632K), 0.0002611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 64K->96K(1536K)] 489K->521K(5632K), 0.0002294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 425K->414K(4096K)] 521K->414K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0037219 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 414K->414K(5632K), 0.0007248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 414K->402K(4096K)] 414K->402K(5632K), [Metaspace: 2688K->2688K(1056768K)], 0.0047190 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
异常:Java heap space
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null
Heap
PSYoungGen total 1536K, used 81K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 7% used [0x00000007bfe00000,0x00000007bfe14760,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 4096K, used 402K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 9% used [0x00000007bfa00000,0x00000007bfa649d8,0x00000007bfe00000)
Metaspace used 2694K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K

Process finished with exit code 0

在这个例子中,首先构造​​MyObject​​​对象,并将其赋值给​​obj​​​变量,构成强引用。然后使用弱引用构造这个​​MyObject​​​对象的软引用,并注册导​​softQueue​​​队列里面。当​​softRef​​​被回收时,会被​​softQueue​​​队列,设置obj=null,删除这个强引用。因此,系统内对​​MyObject​​​对象的引用只剩下软引用。此时显示调用​​GC​​​,通过软引用的get方法,取得​​myObject​​​对象实例的强引用。法相对象未被回收。说明在​​GC​​​充足情况下不会回收软引用对象。
接着申请一块堆大小的的堆空间,并​​​catch​​​异常,从执行结果发现,在这次​​GC​​​后,​​softRef.get()​​​不再返回​​MyObject​​​对象,而是返回​​null​​​。说明,在系统内存紧张的情况下,软引用被回收并且加入注册的引用队列
软引用在我们的日常开发中使用的场景很多,比如商城中商品的信息。某个商品可能会被多人访问,此时我们可以把该商品的信息使用软引用保存。当系统内存足够时,可以实现高速查找,当系统内存不足又会被回收,避免​​​OOM​​的风险。

TIPS: 尽管软引用会在OOM之前被清理,但是,这并不表示full gc 会清理软引用对象。在经过full gc后我们的软引用对象都放入了old区,由于full gc的存在,程序大多数强框下并不会OOM。由于软引用对象占据了老年代的空间,full gc将执行的更为频繁。所以还是建议使用弱引用

当然了,上面的例子是​​OOM​​​之前回收软引用。怎么才能​​full gc​​就回收软引用对象呢?

-XX:SoftRefLRUPolicyMSPerMB // FullGC 保留的 SoftReference 数量,参数值越大,GC 后保留的软引用对象就越多。

当我们设置这个参数值为0时,​​full gc​​就会回收我们的软引用对象了

修改main方法内容

public static void main(String[] args) {

//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
//创建软引用
SoftReference<MyObject> softRef = new SoftReference<>(object, queue);
//检查引用队列,监控对象回收情况
new Thread(new CheckRefQueue(queue)).start();
//删除强引用
object = null;
//手动GC
System.gc();
System.out.println("After new byte[]:Soft Get= " + softRef.get());
}

JVM参数​​-Xmx5m -XX:+PrintGCDetails -Xmn2m -XX:SoftRefLRUPolicyMSPerMB=0​​ 执行结果

[GC (System.gc()) [PSYoungGen: 890K->496K(1536K)] 890K->520K(5632K), 0.0021185 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->413K(4096K)] 520K->413K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0071067 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
After new byte[]:Soft Get= null
Object for SoftReference is null
MyObject's finalize called
Heap
PSYoungGen total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e68,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 4096K, used 413K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa67608,0x00000007bfe00000)
Metaspace used 2693K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K

Process finished with exit code 0

可以发现在手动GC后,软引用对象已经被回收。此时的软引用已经与弱引用效果一样了。下面看弱引用

弱引用

弱引用时一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统对空间是否足够,都会对对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长时间。一旦一个弱引用对象被垃圾收集器回收,便会加入导一个注册引用队列中
修改软引用例子中的main方法内容为

public static void main(String[] args) {

//强引用
MyObject object = new MyObject();
//创建引用队列
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
WeakReference<MyObject> weakRef = new WeakReference<>(object, queue);
new Thread(new CheckRefQueue(queue)).start();
object = null;
System.out.println("After new byte[]:Soft Get= " + weakRef.get());
System.gc();
System.out.println("After new byte[]:Soft Get= " + weakRef.get());
}

​JVM​​​参数:​​-Xmx5m -XX:+PrintGCDetails -Xmn2m​​ 执行结果如下

After new byte[]:Soft Get= I'am MyObject
[GC (System.gc()) [PSYoungGen: 891K->496K(1536K)] 891K->520K(5632K), 0.0016576 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 24K->426K(4096K)] 520K->426K(5632K), [Metaspace: 2685K->2685K(1056768K)], 0.0124398 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null
Heap
PSYoungGen total 1536K, used 71K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe11e60,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 4096K, used 426K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
object space 4096K, 10% used [0x00000007bfa00000,0x00000007bfa6a988,0x00000007bfe00000)
Metaspace used 2692K, capacity 4490K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K

Process finished with exit code 0

通过结果可以看到,在GC之前,弱引用对象并未被垃圾回收器发现,因此通过​​weakRef.get()​​​方法可以获得对应的强引用,但是只要进行垃圾回收,弱引用对象一旦被发现,便会立刻被回收,并加入注册的引用队列中。此时,再通过​​weakRef.get()​​方法取得强引用就会失败

虚引用在我们的日常代码中也经常用到,比如​​ThreadLocal​​​的内部实现就是一个​​ThreadLocalMap​​​,该​​map​​​的​​Entry​​​的​​key​​​为​​ThreadLocal​​​本身,​​value​​​为我们向​​ThreadLocal​​​对象​​set​​​的值,其中的​​key​​​就是弱引用对象。又比如
集合​​​WeakHashMap​​,都是使用了弱引用实现的,想更加深了解的可以区看看相关源码的实现。

虚引用

虚引用时所有引用类型中最弱的一个,一个持有弱引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的​​get()​​方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现他还有虚引用,就会在垃圾回收后销毁这个对象,将这个对象加入引用队列。虚引用主要用于检测对象是否已经从内存中删除。