浅谈Java中的四种引用

Java从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用Strong Reference、软引用SoftReference、弱引用WeakReference和虚引用PhantomReference。可能作为服务端开发人员,如果没有深入底层研究一些常用框架的源码,估计对上面的提及的四种引用可能基本没有什么概念。但是作为Android终端开发人员,谈到Java中的几种引用类型,如果还说不出个所以然来,这样就有些说不过去了,说明基础知识点还有很大的提升空间。

作为终端开发,需要在手机有限的内存上流畅的运行自己开发的APP,可以说最需要考虑的一点就对手机内存的占用。平常开发中四种引用最常用的一个就是弱引用WeakReference,因为一旦垃圾收集器执行一次GC,弱引用的对象就会被回收掉。另外一个常使用的引用就是SoftReference,在LruCache没有出现之前,作为优化缓存Bitmap的一个很有效的方式其实就是使用软引用SoftReference,由于LruCache在缓存上面比SoftReference优势明显许多,软引用SoftReference在优化内存上面渐渐被Google抛弃了。

我们知道在Java中,内存的分配和回收是由JVM负责的,不需要开发者像C语言那样手动操作内存,但是这也是Java语言的缺点,垃圾回收对于开发人员来说是不可控的。有些开发人员可能表示不同意了,我可以手动触发GC,直接调用System.gc(),其实这不能保证一定会立刻执行GC,调动方法System.gc()相当于“建议”执行垃圾回收,但是什么时候调用是不能确定的。

在JDK1.2之前,如果一个对象不被任何变量引用,则程序无法再次使用这个对象,这个对象最终会被GC。但是如果之后可能还会用到这个对象,就只能去新建一个了,这其实就降低了JVM性能,没有达到最大的优化策略。但是JDK1.2以后,可以说在一定程度上面可以控制垃圾回收,至少可以在代码层面控制一下对象生命周期。比如上面说的对象不被任何变量引用,这时候开发人员可以根据需要将其放入软引用或者弱引用,这样既可以达到对象复用又不影响GC。

强引用Strong Reference

强引用是最普遍的引用,如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出OutOfMemoryError异常;只有当这个对象没有被引用时,才有可能会被回收。

public class StrongTest {
	
	static class BigObject {
        private Byte[] bytes = new Byte[1024 * 1024];
    }

    public static void main(String[] args) {
        List<BigObject> list = new ArrayList<>();
        while (true) {
            BigObject obj = new BigObject();
            list.add(obj);
        }
    }
}

在上面代码中BigObject obj = new BigObject()Byte[] bytes = new Byte[1024 * 1024]创建的对象obj和bytes都是强引用类型的,而且这两种类型的对象都是在堆中分配内存,当main方法运行一段时间后就会抛出如下异常:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.yimi.reference.StrongTest$BigObject.<init>(StrongTest.java:9)
	at com.yimi.reference.StrongTest.main(StrongTest.java:16)

如果想中断或者回收强引用对象,可以显式地将引用赋值为null,这样的话JVM就会在合适的时间,进行垃圾回收。由于强引用是使用最为普遍也是最常见的引用类型,将强引用的对象显示赋值为null,这种方式是开发中十分常见的内存优化方式

软引用SoftReference

接下来介绍的三种引用其实都是java.lang.ref.Reference的子类。软引用SoftReference用来描述一些还有用但并非必须的对象,最常用于实现内存敏感的缓存。当内存空间足够,垃圾回收器就不会回收它。当内存空间不足了,就会回收该对象。JVM会优先回收长时间闲置不用的软引用的对象,对那些刚刚构建的或刚刚使用过的“新”软引用对象会尽可能保留。如果回收完还没有足够的内存,才会抛出OutOfMemoryError内存溢出异常。只要垃圾回收器没有回收它,该对象就可以被程序使用。

Reference的子类一般结合队列java.lang.ref.ReferenceQueue<T>使用,在软引用使用过程中,一旦保存在软引用中的对象被GC回收了,这时候持有该对象的引用便会被放入队列ReferenceQueue中。

User user = new User("admin", 20);
ReferenceQueue<? super User> queue = new ReferenceQueue<>();
SoftReference<User> reference = new SoftReference<User>(user, queue);

在这个示例中,一旦在软引用Reference中User对象被GC回收之后,这时候我们再次调动reference.get()方法将会返回null,User对象虽然已经被回收了, 但是持有User对象的引用Reference可能还会存在,仍然有可能造成内存泄漏,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。如何判断Reference对象可回收,ReferenceQueue就起作用了,当User对象被回收之后,Reference对象便会被保存在相对应的队列ReferenceQueue中,当我们调用队列的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。 由于一个Reference对象只能缓存一个对象,所以在开发中作为缓存容器一般和集合框架结合使用,如下是一个结合弱引用的SoftValueMap部分实现。

public class SoftReferenceMap<K, V> extends HashMap<K, V> {
	// 降低V的引用级别到软引用
	private HashMap<K, KeySoftReference<K, V>> temp;

	private ReferenceQueue<V> queue;

	public SoftReferenceMap() {
		// 软引用
		temp = new HashMap<K, KeySoftReference<K, V>>();
		queue = new ReferenceQueue<V>();
	}

	@Override
	public V put(K key, V value) {
		KeySoftReference<K, V> sr = new KeySoftReference<K, V>(value, key, queue);// 将sr与queue绑定
		temp.put(key, sr);
		return null;
	}

	@Override
	public V get(Object key) {
		clearSR();
		KeySoftReference<K, V> sr = temp.get(key);
		if (sr != null) {
			// 如果此引用对象已经由程序或垃圾回收器清除,则此方法将返回 null。
			return sr.get();
		} else {
			return null;
		}
	}

	@Override
	public boolean containsKey(Object key) {
		clearSR();
		KeySoftReference<K, V> sr = temp.get(key);
		// temp.containsKey(key);

		/*
		 * if(sr.get()!=null) { return true; }else{ return false; }
		 */
		if (sr != null) {
			return sr.get() != null;
		}
		return false;

	}

	private void clearSR() {
		// 如果存在一个立即可用的对象,则从该队列中"移除"此对象并返回。否则此方法立即返回 null。
		KeySoftReference<K, V> poll = (KeySoftReference<K, V>) queue.poll();
		while (poll != null) {
			// 从temp将sr强引用对象清除
			temp.remove(poll.key);
			poll = (KeySoftReference<K, V>) queue.poll();
		}
	}

	@Override
	public void clear() {
		temp.clear();
	}

	private class KeySoftReference<K, V> extends SoftReference<V> {
		private Object key;

		public KeySoftReference(V r, Object key, ReferenceQueue<? super V> q) {
			super(r, q);
			this.key = key;
		}
	}
}

在LruCache出现之前,SoftReference在Android开发中还是比较推荐的,但是虽然Android版本迭代升级,SoftReference渐渐被抛弃了,如下是Google官方的介绍,更多可以参看SoftReference

In practice, soft references are inefficient for caching. The runtime doesn't have enough information on which references to clear and which to keep. Most fatally, it doesn't know what to do when given the choice between clearing a soft reference and growing the heap.

在实践中,软引用SoftReference在缓存中是低效的,因为runtime并没有足够的信息来判别应该清除或者保留哪个SoftReference(持有的对象),更无法判定当 App要求更多内存的时候,是应该清除SoftReference,还是增大App的Heap。

做服务端开发和做终端开发,在引用使用的差异性上面主要是由于平台不同造成的。Android Runtime与 JVM 不一样的是:用户App通常没有权限来设定自己的最大可用内存,这个是由系统控制的,单个App使用的最大内存容量是固定的。

从Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。

弱引用WeakReference

弱引用WeakReference在使用方式上面跟软引用非常类似,所不同的是弱引用具有更短的生命周期,它只能生存到下一次垃圾收集发生之前。当垃圾回收器扫描到只具有弱引用的对象时,无论当前内存空间是否足够,都会回收它。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

如下是一个弱引用的简单示例。

public class WeakTest {

	private static WeakReference<User> reference;
	private static ReferenceQueue<? super User> queue = new ReferenceQueue<>();

	public static void main(String[] args) {

		User user = new User("admin", 20);
		reference = new WeakReference<User>(user, queue);
		
		System.out.println(reference.get());
		System.out.println("reference:"+queue.poll());
		
		int i=0;
		while (true) {
			if (reference.get() != null) {
				i++;
				System.out.println("Object is alive for " + i + " loops - " + reference);
			} else {
				System.out.println("Object has been collected.");
				break;
			}
		}
		
	}
}

运行一段时间我们就会发现程序输出了Object has been collected.

弱引用在Android中是应用最为广泛的一种应用,比较常见的一些网络库、图片库以及检查内存泄漏的库LeakCanary都有弱引用的影子,其中LeakCanary用于检测内存泄漏的核心机制就是使用弱引用,当Activity被销毁后会存入弱引用对象中,在GC过后如果该Activity还没有被回收,则说明该Activity会引起内存泄漏。后续有时间另起一篇博文分析一下LeakCanary的实现。

如下是弱引用在Android的UI层最常见的一种用法。

private static class UIHandler extends Handler {

	private final WeakReference<Activity> weakReference;

	public UIHandler(Activity activity) {
		weakReference = new WeakReference<>(activity);
	}

	@Override
	public void handleMessage(Message msg) {
		if (weakReference.get() != null) {
			//TODO
		}
	}
}

在开发中,如果使用弱引用缓存数据,类似软引用使用,我们可以借助于如HashMap集合,自己扩展实现更灵活的缓存框架,当然了也可以直接使用JDK为我们提供的弱引用集合框架。从JDK1.2开始,Java为我们提供了一个WeakHashMap类,内部实现上面其实是维护了一个实现了WeakReference的子类Entry,更多有关WeakHashMap可以参考网上博文,WeakHashMap类在使用上面跟HashMap基本一样。

虚引用PhantomReference

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

小结

强引用时Java中默认的应用形式,使用时不需要显示的使用Reference定义,是平常开发中最常使用的引用形式。弱引用关联的对象在垃圾回收时总是会被回收,被软引用关联的对象只有在内存不足时才会被回收。虚引用的get()方法获取的永远是null,无法获取对象实例。

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 垃圾回收时终止
虚引用 Unkonwn Unkonwn Unkonwn

评论(1)

  • JamesThouh

    We offer you the opportunity to advertise your products and services. Good day! Behold is nice offers for you. I can send your commercial offers or messages through feedback forms.

您确定要删除吗?删除之后不可恢复