本文对应原书条目7,这个条目的标题是“消除过期的对象引用”,书中作者只给出了几条实用建议,但是缺乏原理性知识的讲解,所以我想先针对这块前置知识分享下自己的学习心得,主要是Java语言中的四种引用和引用队列,再就着书中的几条建议说说自己的看法。
Java语言存在四种引用,分别是强引用,软引用,弱引用和虚引用。它们各自有着不同的特点和用途,下面逐一进行介绍。
强引用是最常见的一种引用,不需要显式地进行创建。我们在new一个对象给一个变量的时候,其实就完成了一个强引用的创建。
Object obj = new Object();
如果一个对象拥有强引用,那么它就是 强可达
(strongly reachable)的,任何时候垃圾回收器都不会回收这个对象。事实上,JVM宁愿抛出 OutOfMemoryError
也不会回收这个对象。如果要让它被回收掉,要么显式地把变量(引用)置为null,要么等到超出对象的生命周期范围的时候。怎么理解后者呢?比如在一个方法里new了一个对象,那么在这个方法执行结束后,对象的引用就不存在了,此时这个对象就会被回收。
软引用是一个和内存紧密关联的引用。如果对象只有一个软引用,那么当内存空间足够时,JVM不会回收它,只有当内存吃紧的时候才会回收。以下是创建软引用的一个示例:
// 先创建一个强引用 Object obj = new Object(); // 再创建一个软引用 SoftReference<Object> softRef = new SoftReference<>(obj); // 之后可以把强引用删掉 obj = null; // 可以通过get方法来获取软引用指向的对象 Object obj2 = softRef.get(); // 上面的操作,如果对象已经被回收了,就会返回null,否则返回这个对象,这时对象就多了obj2这个强引用了
弱引用指向的对象也是能够被回收的,只是它的回收策略更严格。如果一个对象只有一个弱引用,那么不管现在内存空间是否足够,这个对象都会被回收掉。以下是创建弱引用的示例:
// 先创建一个强引用 Object obj = new Object(); // 再创建一个弱引用 WeakReference<Object> weakRef = new WeakReference<>(obj); // 把强引用删掉后下次gc就会把弱引用指向的对象回收了 obj = null;
虚引用是最弱的一种引用,拿到这个引用没有任何实际用途。它只是用来“跟踪对象被垃圾回收器回收的活动”。 [2] 虚引用必须得和引用队列一起使用。
引用队列是一个存储除强引用外其他三种引用的队列。引用队列就像是死刑宣判区,在这个队列里的引用所指向的对象,要么即将被回收,要么已经被回收了。因为JVM在执行垃圾回收的时候,可能不是马上执行的,但可以马上把相关的引用放进引用队列里。(这段是我自己的理解,如果不对请指正)我们通过遍历这个队列,就可以知道哪些对象是肯定要被回收的了,这样我们就可以及时止步,不去用这些对象了,或者重新赋予它一个强引用。引用队列可以这么用:
Object obj = new Object(); ReferenceQueue<Object> refQueue = new ReferenceQueue<>(); SoftReference<Object> softRef = new SoftReference<>(obj, refQueue);
软引用、弱引用、虚引用的构造方法里都可以塞一个ReferenceQueue进去,在对象被回收的时候,这个引用就会被加入队列中去。
这四种引用里,强引用不必多说,是我们平常最多用的。虚引用更像是个高级特性,我们一般也不会用到。而软引用和弱引用夹在它们中间,我们有时可能会用到,比如自己实现本地缓存的时候。两者都是指向一些程序运行时非必需的对象的,但是相应的gc策略又不同。那么它们各自适用于哪种场景呢?参考资料[3]给了我一个答案:软引用通常可用于服务端的本地缓存,因为服务端对内存的要求并不苛刻,而且访问量比较大,缓存里的对象过期时间比较长,不应该被频繁回收。而弱引用则可用于移动客户端的缓存或者事件监听器,因为移动设备内存比较小,对gc很敏感,很多资源不用的时候就需要及时回收掉。
有了上面关于Java语言引用的的基本知识,我们可以来看看书上这个条目所想讨论的内容了。其实主要就是我们该如何处理由 过期引用 (obsolete reference)引起的内存泄露问题。 [1]
书中给出了一个自己手写Stack类的例子,像我们写这种类的时候,一定要警惕,因为涉及到内存空间的管理。我们声明了一个数组,管JVM要了一块内存,这块内存里有两部分内容,活动区域和非活动区域。活动区域就是我们栈内的空间,随着每次入栈和出栈都会动态改变,而非活动区域就是栈以外的部分(总空间-活动空间),这个区域里的对象其实都是没有用的,应该要被回收掉的,不然就会占用堆的空间了。所以我们需要在出栈的时候把数组中的这个引用显式地置为null。
不过Joshua Bloch也提醒我们,“清空对象引用应该是一种例外,而不是一种规范行为”。 [1] 只有涉及对内存空间的管理时才需要去考虑清空对象引用。
上面这种情况还好,因为我们只要了堆上一块固定的区域,如果我们用了HashMap这种会自动扩容的类,那问题就严重了,随着时间的推移,Map占的内存会越来越大,而其中的过期对象也会越来越多,最后就会导致内存泄露。这种情况通常会出现在我们自建本地缓存的时候。这个时候我们就要用软引用或弱引用来实现缓存了。软引用缓存需要我们自己去实现,而弱引用可以直接使用 WeakHashMap
。WeakHashMap中的Entry直接继承了WeakReference,所以当外部没有对其某项值的引用时,这个项值就会被回收掉了。
此外我们也可使用LinkedHashMap里的 removeEldestEntry
方法,在每次put的时候,指定一个removeEldestEntry的策略,满足的时候就把最早的项值删去。比如我们可以参考Java自带的ExpiringCache中的使用:
private int MAX_ENTRIES = 200; ExpiringCache(long millisUntilExpiration) { this.millisUntilExpiration = millisUntilExpiration; map = new LinkedHashMap<String,Entry>() { protected boolean removeEldestEntry(Map.Entry<String,Entry> eldest) { return size() > MAX_ENTRIES; } }; }
ExpiringCache中removeEldestEntry的策略是当map的size超过最大值时触发的。而每次put操作的最后都会进行removeEldestEntry。
回调方法这块我比较陌生,应该也是客户端用得比较多。这块内容如果不准确欢迎大家在评论区指正~我理解客户端涉及很多事件监听器的注册,这些事件监听器不应该一直存在堆里,所以也应该用一个WeakHashMap这样的工具类进行统一的维护,如果外部没有事件源对其产生依赖了,就需要及时地被回收掉。
虽然Java是一个自带垃圾回收功能的高级语言,这让程序员省了很多心,不用和内存打太多交道。但其实我们平时遇到的很多事故也和内存泄漏息息相关。知己知彼才能百战不殆。我们在写代码的时候也要注意好(1)这个类是否需要程序员自己去管理内存;(2)是否使用了正确的内存回收策略;(3)是否及时清除了过期引用。
本文仅用于学习交流,请勿用于商业用途。转载请注明出处,如果涉及任何版权问题,请及时与我联系,谢谢!