一、四种引用简单介绍
强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。
软引用
只有在内存不足时,系统会回收软引用对象。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
弱引用
JVM 开始进行垃圾回收,被弱引用关联的对象都会被回收。
虚引用
随时可能被回收,通常用于消息通知。
二、软弱虚引用使用场景
软弱应用
例子1 解决图片缓存问题
下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个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;
}
总的来说,弱引用和软引用的使用场景差不多,对于图片缓存来说,经常使用到的图片,比如默认头像,默认的图片等建议使用软引用,不经常使用的建议弱引用。
例子2 防止内存泄漏问题
A a = new A();
B a = new B(a);
这种情况下,B的构造函数中使用了A,所以即使A=null;也无法回收A对象,必须要使用,A=null,B=null,才可以回收成功。但是我们可以利用弱引用 WeakReference<String> wr = new WeakReference(a);
便可以直接回收A,但是使用B时注意判空,否则可能抛出空指针异常。
例子3 ThreadLocal内存泄漏问题
明白ThreadLocal问题首先我们要明白什么是ThreadLocal,可以理解为每个线程的局部变量,相当于每个线程持有一个本地副本,互相之间不干扰。
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
JDK 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。JDK 的这种实现方案复杂但更安全。
使用 ThreadLocal 为什么可能导致内存泄露呢?
ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。
但是Map中键值对Entry 中的 Value 是被 Entry 强引用的,即便 value 的生命周期结束了,value 也是无法被回收的,导致内存泄露。因为Key=null以后无法跟踪到value,所以无法回收。
线程池中,如何正确使用 ThreadLocal?
在 finally 代码块中手动清理 ThreadLocal 中的 value,调用 ThreadLocal 的 remove()方法。
下面是源码解析
ThreadLocal 核心方法
设置 Thread 对应的 Value 值,首次会创建一个 ThreadLocalMap ,添加 ThreadLocal - Value 到 ThreadLocalMap 中,并且绑定 ThreadLocalMap 到当前线程。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
创建 ThreadLocalMap,绑定到当前线程。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过 ThreadLocalMap 获取当前线程的存储的 Value 值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
设置 ThreadLocal 的初始化值,未 set(T value) 初次获取 Thread 对应的 Value 值时会调用,即被 setInitialValue 方法调用。需要重写该方法。
protected T initialValue() {
return null;
}
移除当前线程存储的 Value 值。当 ThreadLocal 不在使用,最好在 finally 语句块中,调用 remove() 方法,释放去 Value 的引用,避免内存泄露。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}