无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。
在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息,如下面的 SocketManager 类所示:使用一个全局 Map 将元数据关联到一个对象。
publicclass SocketManager {
private Map<Socket, User> m = new HashMap<Socket, User>();
publicvoid setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
returnm.get(s);
}
publicvoid removeUser(Socket s) {
m.remove(s);
}}
这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。
这会阻止 Socket 和 User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。
在 Java 集合中有一种特殊的 Map 类型 — WeakHashMap,在这种 Map 中存放了键对象的弱引用,当一个键对象被垃圾回收器回收时,那么相应的值对象的引用会从 Map 中删除。WeakHashMap 能够节约存储空间,可用来缓存那些非必须存在的数据。关于Map 接口的一般用法。
下面示例中 MapCache 类的 main() 方法创建了一个 WeakHashMap 对象,它存放了一组 Key 对象的弱引用,此外 main() 方法还创建了一个数组对象,它存放了部分 Key 对象的强引用。
import java.util.WeakHashMap;
class Element {
private String ident;
public Element(String id) {
ident = id;
}
public String toString() {
returnident;
}
publicint hashCode() {
returnident.hashCode();
}
publicboolean equals(Object obj) {
return obj instanceof Element && ident.equals(((Element) obj).ident);
}
protectedvoid finalize(){
System.out.println("Finalizing "+getClass().getSimpleName()+" "+ident);
}
}
class Key extends Element{
public Key(String id){
super(id);
}
}
class Value extends Element{
public Value (String id){
super(id);
}
}
public class CanonicalMapping {
publicstaticvoid main(String[] args){
int size=1000;
Key[] keys=new Key[size];
WeakHashMap<Key,Value> map=new WeakHashMap<Key,Value>();
for(int i=0;i<size; i++){
Key k = new Key(Integer.toString(i));
Value v = new Value(Integer.toString(i));
if(i%3==0)
keys[i] = k;
map.put(k, v);
}
System.gc();
}
}
从打印结果可以看出,当执行 System.gc() 方法后,垃圾回收器只会回收那些仅仅持有弱引用的 Key 对象。id 可以被3整除的 Key 对象持有强引用,因此不会被回收。
在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了。(这里假定 SocketManager 不需要线程安全)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。用 WeakHashMap 修复 SocketManager。
public class SocketManager {
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
publicvoid setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
returnm.get(s);
}}
WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。
但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存。
引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有个构造函数取引用队列作为参数。如果用关联的引用队列创建弱引用,在弱引用对象成为 GC 候选对象时,这个引用对象就在引用清除后加入到引用队列中(具体参考上文软引用示例)。
WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。

