引言
Ethan Nicholas 在他的一篇文章中说:他面试了20多个Java高级工程师,他们每个人都至少有5年的Java从业经验,当他问这些工程师对于Weak References 的理解时,只有其中的2个人知道Weak References 的存在,而这2个人中,只有1人知道如何去使用Weak References,而其他人甚至都不知道Java中有Weak References的存在。
大家可能会想,我接触了Java这么多年,从来都没有使用过Weak References啊,它真的是一个有用的技术吗?对于它有用没用,我相信大家看完这篇文章,理解了它以后,自有判断。如果你从业了3年以上的Java开发,你不知道如何使用Weak References也还可以原谅,有可能你的项目还没有那么复杂。但是如果你甚至都没见过它在哪使用的,我觉得你可能读的源码太少了。我相信大家都知道ThreadLocal
这个类吧,你可以去看一下它的静态内部类ThreadLocalMap
, 如果你想见到更多它的应用,我给大家推荐个网站:searchcode,这个网站会根据你输入的代码片段,来搜索几大源码托管平台上使用你输入代码片段的工程,大家可以输入WeakReference
试一试。
并不说只有你成为一个Weak References方面的专家,你才是一个优秀的Java开发者,但是,你至少要了解我们在什么样的场景下需要使用它,That’s enough.
由于理解Weak References && soft references 会涉及到JVM的垃圾收集的一些知识,如果你对这方面没有了解,请你参考我的这篇文章:Hotspot虚拟机- 垃圾收集算法和垃圾收集器
Java中的4种reference
在Java中,有4种reference类型,它们从强到弱,依次如下:
- Strong reference : 大家平常写代码的引用都是这种类型的引用,它可以防止引用的对象被垃圾回收。
- Soft reference : 它引用的对象只有在内存不足时,才会被回收。
- Weak reference : 它并不会延长对象的生命周期,即它不能阻止垃圾收集器回收它所引用的对象。
- Phantom reference : 它与上面的3种类型有很大的不同,它的
get()
方法始终返回null
,即通过这个引用,你甚至都不能获取它所引用的对象,如果你看它的源码,它的构造器必须要给定一个ReferenceQueue
,当然了,你也可以把它设置为空,但是这样的引用 一点意义都没有。我在下文中会结合Phantom reference的作用来解释为什么会这样。
在这一小节中,我总结了各个引用的作用。如果大家不太明白,没关系,我会在下文中更详细地解释它们各自的用法。
Strong reference
如果大家对垃圾收集机制有所了解,你们就会知道JVM标记一个对象是否为垃圾是根据可达性算法。 我们平常写的代码其实都是Strong reference,被Strong reference所引用的对象它会保持这个对象到GC roots的可达性,以防被JVM标记为垃圾对象,从而被回收。比如下面的代码就是一个Strong reference
String str = new String("hello world");
Soft reference
GC使Java程序员免除管理内存的痛苦,但是这并不意味着我们可以不关心对象的生命同期,如果我们不注意Java对象地生命周期,这很可能会导致Java出现内存泄露。
Object loitering
在我详细解释Soft reference之前,请大家先阅读下面的这段代码,仔细想一想它可能出现什么样的问题?
public class LeakyChecksum {
private byte[] byteArray;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len];
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
对于上面的程序而言,如果我把byteArray字节数组放到getFileChecksum
方法中完全没有问题,但是,上面的程序把byteArray字节数组从局部变量提升到实例变量会出现很多问题。比如,由于你需要共享byteArray变量,从而你不得不去考虑线程安全问题,而上面的程序在getFileChecksum
方法上加上了synchronized
关键字,这大大降低了程序的可扩展性。
先不去深入讨论上面程序出现的其它问题,让我们来探讨一下它出现的内存泄露问题。上述代码的主要功能就是根据文件的内容去计算它的checksum,如果上述代码的if
条件不成立,它会不断地重用字节数组,而不是重新分配它。除非LeakyChecksum对象被gc,否则这个字节数组始终不会被gc,由于程序到它一直是可达的。而且更糟糕的是,随着程序的不断运行,这个字节数组只会不断增大,不会减小,它的大小始终都和它处理过的最大的文件的大小一致,这样很可能会导致JVM更频繁地GC,降低应用程序地性能。大多数情况下,这个字节数组所占的空间要比它实际要用的空间要大,而多余的空间又不能被回收利用,这导致了内存泄露。
Soft references 解决上面的内存泄露问题
对于只被Soft references所引用的对象,我们称它为softly reachable objects. 只要可得到的内存很充足,softly reachable objects 通常不会被gc. JVM要比我们的程序更加了解内存的使用情况,如果可得到的内存紧张,那么JVM就会频繁地进行垃圾回收,从而释放更多的内存空间,供我们使用。因此,上述程序的字节数组缓存由于一直是可达的,即使在内存很紧张的情况下,它也不会被回收掉,这无疑给垃圾收集器更大的压力,使其更频繁地GC.
那么有没有一种解决方案可以做到这样呢,如果我们的内存很充足,我们就保持这样的缓存在内存中,不被gc; 但是,当我们的内存吃紧时,就把它释放掉。那么大家想一想,谁可以做到这一点呢?答案是JVM,因为它最了解内存的使用情况,我们可以借助它的力量来达到我们的目标,而Soft references 可以帮我们Java 程序员借助JVM的力量。下面,让我们来看看如果用SoftReference
改写上面的代码。
public class CachingChecksum {
private SoftReference<byte[]> bufferRef;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
byte[] byteArray = bufferRef.get();
if (byteArray == null || byteArray.length < len) {
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
从上面的代码我们可以看出,一旦走出if
语句,字节数组对象就只被Soft references 所引用,成为了softly reachable objects. 对于垃圾收集器来说,它只会在真正需要内存的时候才会去回收softly reachable objects. 现在,如果我们的内存不算吃紧,这个字节数组buffer会一直保存在内存中。在抛出OutOfMemoryError
之前,垃圾收集器一定会clear掉所有的soft references.
Soft references 与缓存
从上面的例子中,我们看到了如何用soft reference 去缓存1个对象,然后让JVM去决定什么时候应该把对象从缓存中清除。对于严重依赖缓存提升性能的应用而言,用Soft references 做缓存并不合适,我们应该去找一个更全面的缓存框架去做这件事。但是由于它 “cheap and dirty” 的缓存机制, 对于一些小的应用场景,它还是很有吸引力的。
Weak References
由于JVM帮我们管理Java程序的内存,我们总是希望当一个对象不被使用时,它会被立即回收,即一个对象的逻辑生命周期要与它的实际生命周期相一致。但是有些时候,由于写程序人的疏忽,没有注意对象的生命周期,导致对象的实际生命周期要比我们期望它的生命周期要长。这种情况叫做 unintentional object retention. 下面我们来看看由实例变量HashMap
导致的内存泄露问题。
HashMap 导致的内存泄露问题
用Map去关联短暂对象的元数据很容易出现unintentional object retention 问题。比如你想关联一个Socket连接与用户的信息,由于你并不能干涉Socket
对象的实现,向里面加用户数据,因此最常用的做法就是用全局Map
来做这样的事。代码如下:
public class SocketManager {
private Map<Socket,User> m = new HashMap<Socket,User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s)