专栏名称: defaultCoder
今天看啥  ›  专栏  ›  defaultCoder

Java的强引用, 软引用, 弱引用, 虚引用的基本概念与应用场景

defaultCoder  · 简书  ·  · 2019-05-26 22:18

文章预览

前言

使用java的大家一定了解, java的内存是由JVM负责分配和回收, 不需要像c语言那样需要由程序员手动管理内存, 这样方便了我们的开发, 使得我们不需要太过于纠结这些内存细节的管理, 有效提升我们开发效率。但这样不代表我们不需要关注内存方面的事, 因为对内存的疏忽, 容易导致OOM(OutOfMemory), 由于内存由JVM负责, 如果我们想要对内存进行稍灵活的控制, 需要我们对JVM的内存回收机制有一定的了解, 了解JVM何时对何种内存进行回收, 本文不对何时回收进行展开, 主要跟大家聊聊JVM对四种引用的回收策略。

JDK1.2, 四种引用的引入

JDK1.2之前, 只要对象不被任何变量引用, 那程序就不能再使用这个对象, 这种规则就是要么用, 要么丢。但有时候我们并不希望这样, 这就像硬盘中当空间足够的时候我们总留着一些大概一两年都不会点开但是又不舍得将它删除的文件, 等到实在是需要空间了, 我们会为了余出空间而清理一遍, 舍弃这些文件。对于java也是如此, 我们有时候希望有些对象就像这些文件, 内存足够的时候就保留, 而内存不足的时候再去回收。
所以, 从JDK1.2开始, 把对象的引用分为四种, 让我们使用对象时能够更加灵活地控制他们的生命周期, 以便于使用对象时兼顾内存。

概念以及对应例子

强引用(StrongReference)

其实强引用是大家最为熟悉的, 我们普通使用的引用方式就是强引用, 如果大家不了解只是因为没有强调这个概念而已。
强引用是使用最普通的应用。如果一个对象具有强引用, 那么GC(Garbage Collection)绝不会回收它。当内存空间不足, 而内存中如果存在强引用, java虚拟机只会抛出OOM, 使程序异常终止, 不会将具有强引用的对象回收来解决内存不足的问题。

String str = new String("xxx");
HashMap hashMap = new HashMap<>();
hashMap.put("key","value");

普通的引用方式就是强引用.

软引用(WeakReference)

软引用就是我在上文中提到的情况所需的引用类型. 如果一个对象只具有软引用, 那么如果内存空间足够, 那么GC是不会对它进行回收的, 而如果内存不足, 就会回收这个对象.
软引用常用的用法: 软引用可以和一个引用队列(ReferenceQueue)联合使用, 如果软引用所引用的对象被GC回收, 那么JVM会将这个软引用加入到与之关联的引用队列中.

// 创建一个对象, 这时为强引用
String str = new String("xxx");
// 将这个对象用软引用对其进行关联
SoftReference<String> softStr = new SoftReference<String>(str);
// 将原有的str强引用断开, 使这个对象仅具有软引用
str = null;
// 我们可以通过get方法来引用得到这个对象, 当对象被回收, 调用get()得到的值为null
softStr.get();  // xxx

这里我们没有和引用队列(ReferenceQueue)联合使用, 软引用不是必须与引用队列联合使用, 而接下来的虚引用必须这样做, 所以将在虚引用的使用方式中展示与引用队列联合使用的方法.

弱引用(WeakReference)

弱引用比软引用的生命更加短暂, 如果一个只具有弱引用的对象, 被GC扫描到, 无论内存是否足够, GC都会将这个对象回收. 不过, 由于GC的线程优先级非常低, 所以不一定会很快把只具弱引用的对象回收.
弱引用可以和一个引用队列(ReferenceQueue)联合使用, 如果弱引用所引用的对象被垃圾回收, JVM就会把这个弱引用加入到与之关联的引用队列中. 这点跟软引用的常用用法没有什么不同.

// 创建一个对象, 这时为强引用
String str = new String("xxx");
// 将这个对象用弱引用对其进行关联
WeakReference<String> weakStr = new WeakReference<String>(str);  
// 将原有的str强引用断开, 使这个对象仅具有弱引用
str = null;
// 我们可以通过get方法来引用得到这个对象, 当对象被回收, 调用get()得到的值为null
System.out.println(weakStr.get());  
// 我们手动调用GC进行回收
System.gc();
// 由于GC优先级过低, 我们将主线程休眠
Thread.sleep(500);
// 我们再次尝试获取该对象, 验证它是否被GC回收      
System.out.println(weakStr.get());

控制台结果为:

xxx
null

可见, 弱引用被GC扫描到就会直接被回收, 无论内存是否足够.

虚引用(PhantomReference)

虚引用比较特殊, 这样的引用就和没有任何引用一样, 随时都可能被GC回收, 但跟没有任何引用又有所区别, 因为我们可以(必须)将它和引用队列(ReferenceQueue)联合使用. 主要用于跟踪对象被GC回收的活动. 当GC准备回收一个对象时, 如果发现它还有虚引用, 就会在回收对象的内存之前, 把这个虚引用加入到与之 关联的引用队列中.

我们可以通过判断引用队列中是否加入了虚引用来了解这个引用对象是否就要被回收了. 如果引用队列中有这个虚引用, 说明它要被回收了, 我们可以在它被回收前采取一些我们想做的事情.

// 创建一个引用队列queue
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 使用虚引用来引用一个new String("xxx")并关联queue
PhantomReference<String> pr = new PhantomReference<String>(new String("xxx"), queue);
System.out.println("调用GC前: ");
// 通过get方法来引用得到这个对象
System.out.println("pr.get()的值为: " + pr.get());
// 调用poll()可以轮询此队列以查看引用对象是否可用.  如果没有进一步延迟可用, 那么它将从队列中删除并返回.  否则, 此方法立即返回null .  
System.out.println("queue.poll()的值为: " + queue.poll());
System.gc();
// 由于GC优先级过低, 我们将主线程休眠
Thread.sleep(500);
System.out.println("调用GC后: ");
System.out.println("pr.get()的值为: " + pr.get());
System.out.println("queue.poll()的值为: " + queue.poll());
System.out.println("再次调用queue.poll()后, 值为: " + queue.poll());

控制台结果为:

调用GC前: 
pr.get()的值为: null
queue.poll()的值为: null
调用GC后: 
pr.get()的值为: null
queue.poll()的值为: java.lang.ref.PhantomReference@15db9742
再次调用queue.poll()后, 值为: null

我们可以看到,
调用GC前:

  • 我们执行pr.get()返回的值为null, 也就如我们所想, 这个这个虚引用可以当作没有引用, 所以取不到该引用的值.
  • 我们执行queue.poll()返回的值为null, 说明引用队列中没有值, 也就是GC没有对任何相关的引用进行回收

调用GC后:

  • 我们执行pr.get()返回的值依旧为null, 这个毫无悬念
  • 我们执行queue.poll()返回的值为java.lang.ref.PhantomReference@15db9742, 说明这个java.lang.ref.PhantomReference@15db9742对象被GC回收了.
  • 我们可以看到再次执行queue.poll()时, 值又为null, 说明队列中的对象已经在我们上一次调用poll()方法时删除了.

需要注意的是, 在队列中的对象没有被poll出来时, 虚引用对象其实一直处于一个不可达但是未回收的状态, 等你poll后才会回收. 大家可以理解为你使用了虚引用, 而虚引用必须使用队列, 这么墨迹的方式你都用了, 必然是想在他回收前做一些操作, 所以java不轻易直接回收掉这个对象, 给你在该对象死之前进行操作的机会.

应用场景

强引用是大家熟知的引用, 就不再对其应用场景过多介绍了, 下面主要针对软引用, 弱引用, 虚引用的应用场景做一个简单介绍.

软引用的应用场景

我们看一个雇员信息查询系统的实例. 我们将使用一个java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息. 作为一个用户, 我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样, 我们在浏览WEB页面的时候也经常会使用“后退”按钮). 这时我们通常会有两种程序实现方式: 一种是把过去查看过的雇员信息保存在内存中, 每一个存储了雇员档案信息的java对象的生命周期贯穿整个应用程序始终; 另一种是当用户开始查看其他雇员的档案信息的时候, 把存储了当前所查看的雇员档案信息的java对象结束引用, 使得GC可以回收其所占用的内存空间, 当用户再次需要浏览该雇员的档案信息的时候, 重新构建该雇员的信息. 很显然, 第一种实现方法将造成大量的内存浪费, 而第二种实现的缺陷在于即使GC还没有进行垃圾收集, 包含雇员档案信息的对象仍然完好地保存在内存中, 应用程序也要重新构建一个对象. 我们知道, 访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素, 如果能重新获取那些尚未被回收的java对象的引用, 必将减少不必要的访问, 大大提高程序的运行速度.

弱引用的应用场景

考虑下面的场景:现在有一个Product类代表一种产品, 这个类被设计为不可扩展的, 而此时我们想要为每个产品增加一个编号. 一种解决方案是使用HashMap<Product, Integer>. 于是问题来了, 如果我们已经不再需要一个Product对象存在于内存中(比如已经卖出了这件产品), 假设指向它的引用为ProductA, 我们这时会给ProductA赋值为null, 然而这时ProductA过去指向的Product对象并不会被回收, 因为它显然还被HashMap引用着. 所以这种情况下, 我们想要真正的回收一个Product对象, 仅仅把它的强引用赋值为null是不够的, 还要把相应的条目从HashMap中移除. 显然“从HashMap中移除不再需要的条目”这个工作我们不想自己完成, 我们希望告诉GC: 在只有HashMap中的key在引用着Product对象的情况下, 就可以回收相应Product对象了. 显然, 根据前面弱引用的定义, 使用弱引用能帮助我们达成这个目的. 我们只需要用一个指向Product对象的弱引用对象来作为HashMap中的key就可以了. 实际上, 对于这种情况, java类库为我们提供了WeakHashMap类, 使用和这个类, 它的键自然就是弱引用对象, 无需我们再手动包装原始对象.

注: WeakHashMap由于是弱引用key, 如果除了该key的引用外, 没有其他的引用存在, 那么就与弱引用一样随时可能被GC回收.

虚引用的应用场景

在介绍其应用场景前, 我们需要了解一下javaObject类有一个finalize方法, 该方法实在一个对象被真正回收之前会被GC自动调用的, 这个方法被设计时是为了在对象回收前可以用于做一些清理工作. 因为java缺乏类似C++那样的析构函数, 就可以通过finalize方法来实现. 本该如此...但是由于GC的运行时间是不确定的, 而且因为该方法由GC调用, GC的线程优先级又过低, 所以这个finalize方法不一定会被执行, 最致命的是你如果在finalize中写一些内容, GC因为这个方法被重写过, 进行调用, 极其其影响GC的性能. 而这个finalize在对象被回收前不一定执行, 但虚引用一定会在回收前被加入到对应的引用队列中, 所以我们可以通过虚引用来做finalize不一定能做到的事. (jdk9中已经将finalize方法标记为deprecated, 官方推荐使用反射包中的cleaner方法来替换原有finalize的实现, 而cleaner方法的底层就是使用的虚引用)

介绍了这么久, 那么我们说一下这个具体的场景把. 什么时候场景需要我们监控这个对象是否被回收? 被回收前需要做什么清理的工作?
其实在java中有堆这个概念大家并不陌生, 但是不知道大家有没有了解过堆外内存这个概念.

堆外内存
堆内内存也叫直接内存, 这部分内存被操作系统直接管辖. 与堆外内存相对反的, 就是堆内内存, 其实也就是我们所熟知的堆内存, 这个内存是受JVM管控的, 堆内内存中的对象在GC过程中可能会被JVM回收, 而堆外内存是指JVM堆外的内存, 这部分内容不受JVM管控, 也就不能被GC回收.

既然是操作系统的内存, 我们JVM都管不了它, 我们能够使用吗? 其实java提供了DirectByteBuffer类让我们可以对直接内存进行操作, DirectByteBuffer是可以被JVM回收的, 但通过它分配的堆外内存却无法被JVM回收, 所以, 我们需要在这个对象被回收前, 将它管辖的堆外内存也进行回收, 避免内存泄漏, 这时候我们就可以通过虚引用来监控它的回收状态, 在他被回收前, 我们将它分配的堆外内存释放.

那么java中关于强, 软, 弱, 虚四种引用就介绍到这里了. 如对文章有疑问或发现文中的错误, 希望大家能够私信我对其进行修改. 感谢阅读.

………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览