想必很多朋友对OOM(OutOfMemory)这个错误不会陌生,而当遇到这种错误如何有效地解决这个问题呢?今天我们就来说一下如何利用软引用和弱引用来有效地解决程序中出现的OOM问题。
一、Reference的概念
从Java SE2开始,就提供了四种类型的引用:强引用、软引用、弱引用和虚引用。Java中提供这四种引用类型主要有两个目的:第一是可以让程序员通过代码的方式决定某些对象的生命周期;第二是有利于JVM进行垃圾回收。下面来阐述一下这四种类型引用的概念。
1. 强引用(StrongReference)
强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用:
Object object = new Object();
String str = "hello";
只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory
错误也不会回收这种对象。如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
2. 软引用(SoftReference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference
类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:
import java.lang.ref.SoftReference;
public class Main {
public static void main(String[] args) {
SoftReference<String> sr = new SoftReference<String>(new String("hello"));
System.out.println(sr.get());
}
}
软引用对象,在响应内存需要时,由垃圾回收器决定是否清除此对象。软引用对象最常用于实现内存敏感的缓存。假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。
软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError
之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。
3. 弱引用(WeakReference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference
类来表示。下面是使用示例:
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
输出:
hello
null
第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
4. 虚引用(PhantomReference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference
类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
5. 利用软引用和弱引用解决OOM问题
前面讲了关于软引用和弱引用相关的基础知识,那么到底如何利用它们来优化程序性能,从而避免OOM的问题呢?下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径 和 相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。在Android开发中对于大量图片下载会经常用到。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
public void addBitmapToCache(String path) {
// 强引用的Bitmap对象
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 添加该对象到Map中使其缓存
imageCache.put(path, softBitmap);
}
public Bitmap getBitmapByPath(String path) {
// 从缓存中取软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 判断是否存在软引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
Bitmap bitmap = softBitmap.get();
return bitmap;
}
二、ReferenceQueue的使用
在java的引用体系中,存在着强引用,软引用,虚引用,幽灵引用,这4种引用类型。在正常的使用过程中,我们定义的类型都是强引用的,这种引用类型在回收中,只有当其它对象没有对这个对象的引用时,才会被GC回收掉。
当我们希望一个对象被GC掉的时候通知用户线程进行额外的处理时,就需要使用引用队列了。ReferenceQueue
即这样的一个对象,当一个对象被GC掉之后,其Reference
对象会被放入ReferenceQueue
中。我们可以从ReferenceQueue
中获取到相应的对象信息,同时进行额外的处理,比如反向操作,数据清理等。
1. 使用ReferenceQueue进行数据监控
一个简单的例子,通过往map中放入10000个对象,每个对象大小为1M字节数组。使用引用队列监控被放入的key的回收情况。代码如下所示:
Object value = new Object();
Map<Object, Object> map = new HashMap<>();
for(int i = 0;i < 10000;i++) {
byte[] bytes = new byte[_1M];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
map.put(weakReference, value);
}
System.out.println("map.size->" + map.size());
这里使用了weakReference
对象,一旦被GC线程发现,就会被回收。另外使用一个线程不断地从队列中获取被gc的数据,代码如下:
Thread thread = new Thread(() -> {
try {
int cnt = 0;
WeakReference<byte[]> k;
while((k = (WeakReference) referenceQueue.remove()) != null) {
System.out.println((cnt++) + "回收了:" + k);
}
} catch(InterruptedException e) {
//结束循环
}
});
thread.setDaemon(true);
thread.start();
结果如下所示:
9992回收了:java.lang.ref.WeakReference@1d13cd4
9993回收了:java.lang.ref.WeakReference@118b73a
9994回收了:java.lang.ref.WeakReference@1865933
9995回收了:java.lang.ref.WeakReference@ad82c
map.size->10000
在这次处理中,map并没有因为不断加入的1M对象由产生OOM异常,并且最终运行结果之后map中的确有1万个对象。表示确实被放入了相应的对象信息。不过其中的key(即weakReference)对象中的byte[]对象却被回收了。即不断new出来的1M数组被gc掉了。
从输出我们看到有9995个对象被gc,即意味着在map的key中,除了weakReference之外,没有我们想要的业务对象。那么在这样的情况下,是否意味着这9995个entry,我们认为就是没有任何意义的对象,那么是否可以将其移除掉呢。同时还期望size值可以打印出5,而不是10000。WeakHashMap就是这样的一个类似实现。
2. WeakHashMap
weakHashMap即使用weakReference当作key来进行数据的存储,当key中的引用被gc掉之后,它会自动(类似自动)的方式将相应的entry给移除掉,即我们会看到size发生了变化。
从简单来看,我们认为其中所有一个类似的机制从queue中获取引用信息,从而使得被gc掉的key值所对应的entry从map中被移除。这个处理点就在我们调用weakhashmap的各个处理点中,比如get,size,put等。简单点来说,就是在调用get时,weakHashMap会先处理被gc掉的key值,然后再处理我们的业务调用。
简单点代码如下:
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();
return size;
}
此处的expungeStaleEntries即移除方法,具体的逻辑可以由以下的流程来描述:
A: 使用一个继承于WeakReference的entry对象表示每一个kv对,其中的原引用对象即我们在放入map中的key值
B: 为保证效率以及尽可能的不使用key值,hash经过预先计算。这样在定位数据及重新get时不再需要使用原引用对象
- C: 由queue拿到的事件对象,即这里的entry值。通过entry定位到具体的桶位置,通过链表计算将entry的前后重新连接起来(即p.pre.next = p.next)
因此,这里的引用处理并不是自动的,其实是我们在调用某些方法的时候处理,所以我们认为它不是一种自动的,只是表面上看起来是这种处理。
具体的代码,即将开始的map定义为一个WeakHashMap,最终的输出类似如下所示:
9993回收了:java.lang.ref.WeakReference@12aa816
9994回收了:java.lang.ref.WeakReference@2bd967
9995回收了:java.lang.ref.WeakReference@13e9593
weakHashMap.size->4
在上面的代码中,由于weakhashmap不允许自定义queue,所以上面的监控是针对value的。在weakHashMap中,queue在weakhashmap在内部定义,并且由内部消化使用了。如果我们在自己进一步处理,那就只能自定义类似weakHashMap实现,或者使用反向操作。即在监控到变化之后,自己处理map的kv。