概述
我们知道,在Java中,如果变量是一个对象类型的,那么它实际上存放的是对象的引用,但是如果是一个基本类型,那么存放的就是基本类型的值。实际上我们平时代码中类似于Object o = new Object()这样的的引用类型,细分之后可以称为强引用。
如果方法中存在这样的强引用类型,现在需要回收强引用所指向的对象,那么要么此方法运行结束,要么引用连接断开,否则被引用的对象是无法被判定为可回收的,因为我们说不定后面还要使用它。
所以,当JVM内存空间不足时,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
除了强引用之外,Java也为我们提供了三种额外的引用类型。
软引用
软引用不像强引用那样不可回收,当 JVM 认为内存不足时,会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。当然,如果内存充足,那么是不会轻易被回收的。
我们可以通过以下方式来创建一个软引用:
// 强引用写法:Object obj = new Object();
// 软引用写法:
SoftReference<Object> reference = new SoftReference<>(new Object());
//使用get方法就可以获取到软引用所指向的对象了
System.out.println(reference.get());
可以看到软引用还存在一个带队列的构造方法,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
这里我们来进行一个测试,首先我们需要设定一下参数,来限制最大堆内存为10M,并且打印GC日志:
public static void main(String[] args) {
// 引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 软引用
SoftReference<Object> reference = new SoftReference<>(new Object(), queue);
// 获取软引用对象
System.out.println(reference);
try{
List<Byte[]> list = new ArrayList<>();
while (true) {
// 每次1m
list.add(new Byte[1024*1024]);
}
} catch (Throwable t){
System.out.println("发生了内存溢出!"+t.getMessage());
System.out.println("软引用对象:"+reference.get());
System.out.println(queue.poll());
}
}
添加虚拟机参数
-XX:+PrintGCDetails -Xms10m -Xmx10m
运行结果如下:
java.lang.ref.SoftReference@1b6d3586
[GC (Allocation Failure) [PSYoungGen: 1744K->488K(2560K)] 5840K->4784K(9728K), 0.0010459 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 488K->496K(2560K)] 4784K->4848K(9728K), 0.0008931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 4352K->4701K(7168K)] 4848K->4701K(9728K), [Metaspace: 3251K->3251K(1056768K)], 0.0042986 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 4701K->4701K(9728K), 0.0003501 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 4701K->4683K(7168K)] 4701K->4683K(9728K), [Metaspace: 3251K->3251K(1056768K)], 0.0038947 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
发生了内存溢出!Java heap space
软引用对象:null
java.lang.ref.SoftReference@1b6d3586
Heap
PSYoungGen total 2560K, used 85K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd15510,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 4683K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 65% used [0x00000000ff600000,0x00000000ffa92fc8,0x00000000ffd00000)
Metaspace used 3274K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
进程已结束,退出代码0
可以看到,当内存不足时,软引用所指向的对象被回收了,所以get()方法得到的结果为null,并且软引用对象本身被丢进了队列中。
弱引用
弱引用比软引用的生命周期还要短,在进行垃圾回收时,不管当前内存空间是否充足,都会回收它的内存。
我们可以像这样创建一个弱引用:
WeakReference<Object> reference = new WeakReference<>(new Object());
System.out.println(reference.get());
使用方法和软引用是差不多的,但是如果我们在这之前手动进行一次GC:
SoftReference<Object> softReference = new SoftReference<>(new Object());
WeakReference<Object> weakReference = new WeakReference<>(new Object());
//手动GC
System.gc();
System.out.println("软引用对象:"+softReference.get());
System.out.println("弱引用对象:"+weakReference.get());
运行结果如下:
软引用对象:java.lang.Object@1b6d3586
弱引用对象:null
WeakHashMap正是一种类似于弱引用的HashMap类,如果Map中的Key没有其他引用那么此Map会自动丢弃此键值对。
Integer a = new Integer(1);
WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>();
weakHashMap.put(a, "test");
System.out.println(weakHashMap);
a = null;
System.gc();
System.out.println(weakHashMap);
输出结果如下:
{1=test}
{}
可以看到,当变量a的引用断开后,这时只有WeakHashMap本身对此对象存在引用,所以在GC之后,这个键值对就自动被舍弃了。所以说这玩意,就挺适合拿去做缓存的。
虚引用(鬼引用)
虚引用相当于没有引用,随时都有可能会被回收。
看看它的源码,非常简单:
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
也就是说我们无论调用多少次get()方法得到的永远都是null,因为虚引用本身就不算是个引用,相当于这个对象不存在任何引用,并且只能使用带队列的构造方法,以便对象被回收时接到通知。
价值:当gc 回收一个对象时,如果gc 发现此对象还有一个虚引用,就会将虚引用放入到引用队列中,之后(当虚引用出队后)再去回收改对象。
GC->如果有虚引用->虚引用入队->虚引用出队-> 回收该对象。
public static void main(String[] args) throws InterruptedException {
Object my = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference =
new PhantomReference<>(my,referenceQueue);
// 打印my 对象
System.out.println("my = " + my);
// 打印虚引用对象
System.out.println("虚引用对象 = " + phantomReference);
my = null;
// gc 之前,队列为空
System.out.println("gc之前获取队列值为:" + referenceQueue.poll());
// 开始gc,这个不一定立马就gc
System.gc();
// 增加延时,确保gc
Thread.sleep(20);
// gc 后,被回收对象入队
System.out.println("gc之后获取队列值为:" + referenceQueue.poll());
}
输出如下:
my = java.lang.Object@1b6d3586
虚引用对象 = java.lang.ref.PhantomReference@4554617c
gc之前获取队列值为:null
gc之后获取队列值为:java.lang.ref.PhantomReference@4554617c
总结
最后,Java中4种引用的级别由高到低依次为: 强引用 > 软引用 > 弱引用 > 虚引用