Java 中除了八种基本类型(byte, short, int, long, float, double, char, boolean)之外就是引用类型,而引用类型又有强引用,软引用,弱引用和幻象引用。可能我们平常代码中用到最多的是强引用,对于其他的一般用的很少,但有些框架中还是会用到的,所以我们还是有必要去了解下这块的东西,这有利于我们更好地去理解相关的框架原理。下面我们就来分别看下这几类引用都各有什么特性:
声明:下面的代码测试是基于 java 11.0.8 版本下运行。
强引用
强引用算是我们经常使用的,也是我们平常代码中常见的普通对象引用,比如说下面代码中的对象引用:
1 | ReferenceObject referenceObject1 = new ReferenceObject(); |
上面代码中 ReferenceObject 是一个普通的对象,referenceObject1 就是强引用,强引用所引用的对象实例不会被 GC 回收,JVM 宁愿抛出OutOfMemoryError 运行时错误让程序异常终止,也不会回收强引用所指向的对象实例。随后显式地将 referenceObject1 赋值为 null 之后,那么当第一行中创建的对象实例已经没有引用指向该实例了,那么 GC 将会回收该对象实例所在的内存空间。
除了强引用之外,剩下的软引用,弱引用,幻象引用(虚引用)都是继承自 Reference 抽象类,对应的类分别是 SoftReference,WeakReference,PhantomReference,下面我们一个一个看下这几个引用。
软引用
软引用,也就是通过 SoftReference 来实现,这类引用相对强引用来说更弱一些,这类引用在内存充足的情况下不会被回收,通常是在内存不足产生 OutOfMemoryError 之前 GC 将决定回收这类引用所指向的对象实例。下面让尝试模拟这种情况的发生:
1 | String soft = new String("soft"); |
上面代码中第一行创建的 String 对象实例在将 soft 强引用显式地赋值为 null 之后就只有 softreference 软引用指向该对象实例了,然后就算我们主动发起 GC,GC 在内存充足的情况下也不会去回收软引用指向的这个对象实例,这时是还可以通过 softReference 的 get 方法来获取该对象的。接下来我们创建更多的对象实例模拟消耗更多的内存,直到内存不足产生 OutOfMemoryError 之前 GC 将会回收 softReference 指向的对象实例,一旦 GC 决定回收这类引用指向的对象实例,那么我们再去通过 get 方法获取该对象将会返回 null。
1 | List<String> list = new ArrayList<>(); |
上面代码中是模拟创建无限多的对象来占用更多的内存,直到 GC 决定将 softReference 引用指向的对象实例才停止创建,也就是我们通过 get 方法得到的是个 null 来判断 GC 已经决定将该对象实例进行回收。
注意:上面的代码最好是加上 JVM 启动参数 ‘-Xms3m -Xmx3m’ 来配置堆内存的初始大小和最大堆内存为 3m,不然可能需要执行有一段时间才会停下来。
软引用一般适合用来缓存那些在内存中比较难以构建的数据,一旦构建之后就会缓存下来,一般也不会将该缓存删掉,只会在内存不足的情况下才会移除该缓存。
弱引用
弱引用是通过 WeakReference 实现,和 SoftReference 比较类似,只是它相对软引用来说更弱,在下一次的 GC 就会将这类引用指向的对象回收掉,而不会像软引用一样只会在内存不足的情况下才决定回收。同样的看下面代码测试:
1 | String weak = new String("weak"); |
上面代码中 weakReference 所指向的对象实例在发起一次 GC 之后再次通过 get 方法返回的就是 null 了。
对于弱引用 Java 还提供了一个 WeakHashMap 的数据结构,和我们常用的 HashMap 很像,区别就是它是使用 WeakReference 作为 key,而且如果 WeakHashMap 中 key 被 GC 回收之后,对应的 entry 也会被自动移除。可以通过下面的代码来验证:
1 | String weakKey = new String("weakKey"); |
通过上面的代码测试可以看到在 GC 决定将弱引用所指向的对象进行回收之后,相应的 weakHashMap 中的 entry 也被自动移除了,weakHashMap 的大小为 0。这个特性其实可以用于创建本地锁,锁在使用时不会被回收,一旦锁使用完了之后一段时间就可能会被回收掉了,下次再次获取时不存在则创建,存在则直接返回即可,也就是将创建的锁资源交给 JVM 来管理,这样就避免了大量锁创建之后没有得到及时的回收占用太多的内存空间。
同样的,和软引用一样,弱引用也适合用来作缓存,只不过适用的场景不一样,弱引用则适合那种资源创建快速,同时缓存时间较短的情况。
幻象引用
对于幻象引用,也可以称为虚引用,这类引用通过 get 方法得到的始终是个 null,防止恢复一个几乎已经删除的对象。幻象引用一般是配合引用队列一块使用,不然好像也没有什么其他用处了,当幻象引用所指向的对象的物理内存被回收时,幻象引用将会进入队列,接下来就可以轮询该队列做一些对象内存被回收之后的动作。可通过下面的代码测试验证:
1 | ReferenceObject referenceObject = new ReferenceObject(); |
从上面代码中可以看到幻象引用调用 get 方法得到的是个 null,在产生 GC 之后,幻象引用指向的对象物理内存被回收,幻象引用进入队列,通过队列的 poll 方法拿到的和幻象引用的地址是相同的。
注:这里你可以尝试在 ReferenceObject 类中重写 finalize 方法,finalize 方法中先采用空实现看上面的运行结果,然后在 finalize 方法中加上调用父类的 finalize 方法的操作,这时再看运行结果,可以去研究下为什么会出现这种情况(我也还没搞懂为啥,可能和 JVM 的优化有关吧)。
幻象引用的作用主要是能够确定对象何时从内存中删除,当我们有一个业务操作需要分配比较大的内存时,我们就可以采用幻象引用来监视这个操作,通过幻象引用来实现只有当上一个业务操作分配的大内存被回收了之后才进行下一次的大内存分配操作,这样就能避免上一个业务操作分配的大内存还没被回收再次分配一个大内存有可能导致 OOM 的情况发生。
好了,到这里引用的四种类型就已经介绍完了,从上面的整体描述来看,引用类型主要是和 GC 回收引用指向的对象的时机有关,不同的引用类型所指向的对象实例在不同的时间点,不同的条件下被回收,然后我们就可以根据这些特性来做匹配我们业务类型的事情,其实我是想通过这个来引出接下来我想写的和锁有关的知识点,上面弱引用中我已经稍微提到了,下篇文章开始我们就一起来看下和锁相关的介绍。