强引用 软引用 弱引用 虚引用
前言
在平常的开发中用到的几乎都是强引用,但是面试时会问到的却都是其他三种不常用的引用。
通用内部测试类,测试运行时添加限制堆内存大小参数:-Xmx200m
static class Vince {
// 创建100M大小的字节数组
private byte[] bytes = new byte[1024*1024*100];
}
1、强引用
对于一个对象,如果有强引用指向它,那么它不会被垃圾回收掉,就算内存不够用了,也不会被回收掉,只会抛出OOM异常。
测试方法
private static void strongReference() {
Vince vince = new Vince();
Vince vince1 = new Vince();
}
依次创建两个强引用对象,在创建第一个对象时,消耗100M内存,创建第二个对象,还需要100M内存,此时内存不足,抛出OOM异常。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.patent.demo.reference.ReferenceTest$Vince.<init>(ReferenceTest.java:112)
at com.patent.demo.reference.ReferenceTest.strongReference(ReferenceTest.java:40)
at com.patent.demo.reference.ReferenceTest.main(ReferenceTest.java:22)
2、软引用
对于一个对象,如果有软引用指向它,内存足够时,垃圾回收不会回收掉它,但是当内部不足时,垃圾回收会回收掉软引用对象。
测试方法
(1)、先创建强引用对象,再创建软引用对象时,内存不足以创建软引用对象,会抛出OOM异常;
private static void softReference() {
Vince vince = new Vince();
SoftReference<Vince> reference = new SoftReference<Vince>(new Vince());
System.out.println(reference.get());
}
(2)、先创建软引用对象,再创建强引用对象时,内存不足时,软引用对象会被垃圾回收;
private static void softReference_1() {
SoftReference<Vince> reference = new SoftReference<Vince>(new Vince());
Vince vince = new Vince();
System.out.println(reference.get()); // null 被回收了
}
3、弱引用
对于一个对象,如果有弱引用指向它,不管内存够不够用,在垃圾回收的时候就会被回收掉;
测试方法
private static void weakReference() {
WeakReference<Vince> reference = new WeakReference<>(new Vince());
System.out.println(reference.get()); // com.patent.demo.reference.ReferenceTest$Vince@2328c243
System.gc();
System.out.println(reference.get()); // null 被回收了
}
4、虚引用
虚引用和其他三种引用的使用方法都不一样,先介绍使用方法,以下是使用方法:
private static void phantomReference() throws InterruptedException {
// 引用队列
ReferenceQueue<Vince> referenceQueue = new ReferenceQueue<>();
PhantomReference<Vince> reference = new PhantomReference<>(new Vince(), referenceQueue);
System.out.println(reference.get()); // null
}
可以看到,相比于软引用和弱引用,虚引用多了一个 ReferenceQueue (引用队列),虚引用要搭配 ReferenceQueue 使用;执行该方法时,最终输出的内容为null,因为虚引用实在是太虚了,无法通过这种方式获取到虚引用的值。
虚引用的作用是什么呢?
当垃圾回收时,虚引用会被回收掉,同时虚引用会被放进引用队列中,因此可以用虚引用来记录哪些对象被垃圾回收。
测试方法
private static void phantomReference() throws InterruptedException {
ReferenceQueue<Vince> referenceQueue = new ReferenceQueue<>();
PhantomReference<Vince> reference = new PhantomReference<>(new Vince(), referenceQueue);
System.out.println(reference.get()); // null
System.out.println(referenceQueue.poll()); // null
System.gc();
Thread.sleep(1000);
System.out.println(referenceQueue.poll()); // java.lang.ref.PhantomReference@2328c243
}
NIO 经典使用场景
虚引用比较经典的使用场景是NIO的 ByteBuffer,模拟下具体场景:
private static void phantomReference_1() {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 100);
// 置空,方便垃圾回收
buffer = null;
// 垃圾回收后,直接内存也被释放了
System.gc();
}
执行该方法,最后可以发现,垃圾回收之后,直接内存也被释放了,怎么做到的呢?
实际上,在 allocateDirect 方法内部,使用了 Cleaner 类,这个类是 JDK 提供的,它继承了 PhantomReference 类,即虚引用。
allocateDirect 方法源码
// ByteBuffer 类
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// DirectByteBuffer 类
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用了 Cleaner 类
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
// Deallocator 类中的run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 释放直接内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
Cleaner 类,我们自己也可以这么使用:
private static void phantomReference_2() {
Vince vince = new Vince();
Cleaner cleaner = Cleaner.create(vince, new Runnable() {
@Override
public void run() {
System.out.println("123456");
}
});
vince = null;
System.gc();
System.out.println("789789");
}
利用Cleaner生成了一个虚引用指向Vince对象,然后通过gc把Vince对象回收掉,但在真正回收之前,会先调用Cleaner中设置的Runnable对象的run方法,相当于一种回调。
而在ByteBuffer中,就是利用了这种机制。对于ByteBuffer对象,有一个强引用指向它,同时也有一个Cleaner虚引用指向它,同时指定Deallocator对象作为回调,Deallocator中的run方法会进行释放直接内存,只要把强引用ByteBuffer置空,就会进行垃圾回收,并执行Deallocator中的run方法释放直接内存。
总结
- 1、强引用,不会被垃圾回收;
- 2、软引用,当内存不足时,会被垃圾回收;
- 3、弱引用,当执行垃圾回收时,就会被直接回收掉;
- 4、虚引用,要搭配ReferenceQueue使用,可以用来记录某一对象是否被垃圾回收了。