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 官方的介绍, 更多可以参看.
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 |
来源: https://juejin.im/entry/5c88cd1ee51d454a3a0d3fc0