Java虚拟机之强引用、软引用、弱引用、虚引用详解

背景

在JDK 1.2以前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下,只有被引用或者没有被引用这两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们对于这样一类对象需要一种新的引用方式:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都是符合这样的应用场景。

所以,JDK 1.2对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种的引用强度依次逐渐减弱。

强引用

强引用是指在程序代码中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。

软引用

软引用用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

应用场景:软引用通常用来实现内存敏感的缓存。如果内存空间还有剩余,就可以暂时保留缓存,当内存不足时,就可以将缓存清除,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

应用场景:弱应用同样可用于内存敏感的缓存。与软引用不同的是,在内存空间还足够时,软引用的对象不会被回收,但是,弱引用的对象有可能会被回收,其存活时间相比于弱引用更短一点。

虚引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾回收器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

应用场景:可以用虚引用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

软引用、弱引用和虚引用可以和一个引用队列(ReferenceQueue)联合使用,如果它们所引用的对象被垃圾回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。采用软引用、弱引用和虚引用可以预防Java虚拟机抛出OOM异常以及内存泄漏的问题。

软引用、弱引用和虚引用存在的必要性

在Java程序中,如果我们想回收一个对象,可以把对象引用置为null,这样,在下次垃圾回收之前,就可以回收该对象,如下所示:

Object object = new Object();
object = null;

但是手动置为null这种方式太繁琐,而且我们编写程序时也不能一直保证对引用的置空操作是正确的,这种方式完全违背了Java虚拟机自动回收垃圾的概念。同时,采用该种方式会存在以下问题:

对于需要系统缓存的Java程序,在程序运行期间,缓存内容就不能被垃圾回收器回收,如果我们已经不再需要某项缓存,那将其置为null,是不是就可以了呢?

答案是不行的,若缓存使用HashMap<Product, Integer>来实现,其中,key代表一种商品缓存信息,value为商品剩余数目,当商品售空时,我们将指向Product商品的引用(假设为productRef)productRef置为null,然而垃圾回收器并不会回收Product对象,因为在HashMap的Node结点中,仍然保留着一个对Product对象的引用,若是想要回收该对象,必须还得将该条目从HashMap中删除,否则会产生内存泄漏问题。这同样违背了Java虚拟机自动回收垃圾的概念。

对于上述的问题,我们都可以通过软引用和弱引用解决。对于使用单个引用的对象,示例代码如下所示:

public class ReferenceTest1 {
	// 循环检测引用队列是否有值
	static class CheckReferenceQueueThread extends Thread {
		private ReferenceQueue<Object> referenceQueue;
		
		public CheckReferenceQueueThread(ReferenceQueue<Object> referenceQueue) {
			this.referenceQueue = referenceQueue;
		}
		
		@Override
		public void run() {
			super.run();
			
			while (!Thread.interrupted()) {
				Reference<? extends Object> reference = referenceQueue.poll();
				if (reference != null) {
					System.out.println("弱引用对象已被回收,已被加入到引用队列中");
				}
			}
		}
	}
	
	// 循环获取弱引用指向的对象
	static class GetValueThread extends Thread {
		private WeakReference<Object> weakReference;
		
		public GetValueThread(WeakReference<Object> weakReference) {
			this.weakReference = weakReference;
		}
		
		@Override
		public void run() {
			super.run();
			
			while (!Thread.interrupted()) {
				Object object = weakReference.get();
				if (object == null) {
					System.out.println("弱引用对象已被回收,重新为弱引用赋值");
					weakReference = new WeakReference<>(new Object());
				}
			}
		}
	}
	
	private static final ReferenceQueue<Object> REFERENCE_QUEUE = new ReferenceQueue<>();

	public static void main(String[] args) throws InterruptedException {
		GetValueThread getValueThread = new GetValueThread(getWeakReference());
		CheckReferenceQueueThread checkReferenceQueueThread = new CheckReferenceQueueThread(REFERENCE_QUEUE);
		
		getValueThread.start();
		checkReferenceQueueThread.start();
		
		Thread.sleep(1000);
		System.gc();
		Thread.sleep(1000);
		
		getValueThread.interrupt();
		checkReferenceQueueThread.interrupt();
	}
	
	private static WeakReference<Object> getWeakReference() {
		Object object = new Object();
		WeakReference<Object> weakReference = new WeakReference<Object>(object, REFERENCE_QUEUE);
		return weakReference;
	}
}

运行结果为:

弱引用对象已被回收,重新为弱引用赋值
弱引用对象已被回收,已被加入到引用队列中

可以看到,被引用的对象可以自动被垃圾回收器回收,若此时我们仍需要使用此对象,则可以重新生成对象。如果是缓存的话,可以对缓存信息进行重新读取。

同样,对于使用Map引用的对象,Java类库为我们提供了WeakHashMap类,使用和这个类,它的键自然就是弱引用对象,无需我们再手动包装原始对象。当一个键对象被垃圾回收器回收时,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。

WeakHashMap的详解可以参考这篇博客

参考资料

周志明:《深入理解Java虚拟机》

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80460945