1)使用软引用阻止泄漏
在Java语言中有一种形式的内存泄漏称为对象游离(Object Loitering):
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);
// 计算该文件的值然后返回该对象
}
}
上边的代码是类LeakyChecksum用来说明对象游离的概念,里面有一个getFileChecksum()方法用来计算文件内容校验和,getFileCheckSum方法将文件内容读取到缓冲区中计算校验和。来看内存泄漏,该类将在一个程序中被调用许多次,重用缓冲区而不是重新分配它。但是结果是,缓冲区永远不会被释放,因为它对程序来说总是可及的(除非LeakyChecksum对象被垃圾收集了)。
这里可以提供一种策略就是使用Java里面的软引用,软引用对于垃圾收集器来说“只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。” 垃圾收集器在可以抛出OutOfMemoryError之前需要清除所有的软引用。通过使用一个软引用来管理高速缓存的缓冲区,可以解决LeakyChecksum中的问题,如下边代码所示。现在,只要不是特别需要内存,缓冲区就会被保留,但是在需要时,也可被垃圾收集器回收:
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);
}
}
CachingChecksum使用一个软引用来缓存单个对象,并让 JVM 处理从缓存中取走对象时的细节。类似地,软引用也经常用于 GUI 应用程序中,用于缓存位图图形。是否可使用软引用的关键在于,应用程序是否可从大量缓存的数据恢复。如果需要缓存不止一个对象,可以使用一个Map,但是可以选择如何使用软引用。可以将缓存作为 Map<K,SoftReference<V>> 或SoftReference<Map<K,V>>管理。后一种选项通常更好一些,因为它给垃圾收集器带来的工作更少,并且允许在特别需要内存时以较少的工作回收整个缓存。
2)使用弱引用堵住内存泄漏
全局Map造成的内存泄漏,无意识对象保留最常见的原因是使用Map将元数据与临时对象相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建Socket时是不知道这些信息的,并且不能将数据添加到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){
m.remove(s);
}
}
SocketManager socketManager;
//...
socketManager.setUser(socket,user);
这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map中删除相应的映射,否则,Socket和User对象将会永远留在 Map中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候Socket不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。
程序有内存泄漏的第一个迹象通常是它抛出一个OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以-verbose:gc或者-Xloggc选项调用JVM,那么每次GC运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录GC使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用GC日志是值得的。
示例中,SocketManager 的问题是Socket-User映射的生命周期应当与Socket的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏——利用弱引用。弱引用是对一个对象的引用(称为referent)的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable))WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用get()获取它的值。如果弱引用被清除了(不管是referent 已经被垃圾收集了,还是有人调用了WeakReference.clear()),get() 会返回null。相应地,在使用其结果之前,应当总是检查get() 是否返回一个非null值,因为 referent 最终总是会被垃圾收集的。用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到JDK1.2的类库中,它对键(而不是对值)使用弱引用。如果在一个普通HashMap中用一个对象作为键,那么这个对象在映射从Map中删除之前不能被回收,WeakHashMap使您可以用一个对象作为Map键,同时不会阻止这个对象被垃圾收集。下边的代码给出了WeakHashMap的 get()方法的一种可能实现,它展示了弱引用的使用:
public class WeakHashMap<K,V> implements Map<K,V>{
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>{
private V value;
private final int hash;
private Entry<K,V> next;
// ...
}
public V get(Object key){
int hash =getHash(key);
Entry<K,V> e =getChain(hash);
while(e != null){
k eKey = e.get();
if( e.hash == hash&& (key == eKey || key.equals(eKey)))
return e.value;
e = e.next;
}
return null;
}
}
调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在while循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法——一些内部对象扩展 WeakReference。在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为null变得比平时更重要了。
在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下边代码所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。
public class SocketManager{
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User s){
m.put(s,u);
}
public User getUser(Socket s){
return m.get(s);
}
}
引用队列:
WeakHashMap用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get()实现可以根据WeakReference.get()是否返回null来区分死的映射和活的映射。但是这只是防止Map的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从Map中删除死项。否则,Map会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry和值对象也不会被收集。可以通过周期性地扫描Map,对每一个弱引用调用get(),并在get()返回null时删除那个映射而消除死映射。但是如果Map有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列的作用。引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是referent)就在引用清除后加入到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 ——polled、timed blocking 和 untimed blocking。)WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。