Java 理论与实践
清单3显示分解了应用程序内存使用的hprof输出的相关部分。(hprof工具在应用程序退出时,或者用kill-3或在Windows中按Ctrl+Break时生成使用分解。)注意两次快照相比,Map.Entry、Task和int[]对象有了显著增加。
清单3.HPROF输出,显示Map.Entry和Task对象的增加
SITESBEGIN(orderedbylivebytes)FriOct2816:30:482005
percentlivealloc'edstackclass
rankselfaccumbytesobjsbytesobjstracename
170.13%70.13%569488813909569488813909300305int[]
218.27%88.40%14839766827827363213908300321int[]
34.11%92.51%3338161390933381613909300310java.util.HashMap$Entry
42.74%95.25%2225441390922254413909300304com.quiotix.dummy.MapLeaker$Task
52.42%97.67%196640226219211300325java.util.HashMap$Entry[]
60.66%98.33%53680335522246413904300324java.util.concurrent.LinkedBlockingQueue$Node
SITESEND
![]()
SITESBEGIN(orderedbylivebytes)FriOct2816:31:322005
percentlivealloc'edstackclass
rankselfaccumbytesobjsbytesobjstracename
177.07%77.07%4117602410002041176024100020301069int[]
212.98%90.05%69337683592001885688100020301093int[]
34.49%94.55%24004801000202400480100020301082java.util.HashMap$Entry
43.00%97.54%16003201000201600320100020301068com.quiotix.dummy.MapLeaker$Task
51.96%99.50%10485921209724814301104java.util.HashMap$Entry[]
60.05%99.55%2593616211600240100015301101java.util.concurrent.LinkedBlockingQueue$Node
SITESEND
清单4展示了hprof输出的另一部分,给出了Map.Entry对象的分配点的调用堆栈信息。这个输出告诉我们哪些调用链生成了Map.Entry对象,并带有一些程序分析,找出内存泄漏来源一般来说是相当容易的。
清单4.HPROF输出,显示Map.Entry对象的分配点
TRACE300446:
java.util.HashMap$Entry.<init>(<UnknownSource>:Unknownline)
java.util.HashMap.addEntry(<UnknownSource>:Unknownline)
java.util.HashMap.put(<UnknownSource>:Unknownline)
java.util.Collections$SynchronizedMap.put(<UnknownSource>:Unknownline)
com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)
弱引用来救援了
SocketManager的问题是Socket-User映射的生命周期应当与Socket的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从JDK1.2开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏——利用弱引用。
弱引用是对一个对象(称为referent)的引用的持有者。使用弱引用后,可以维持对referent的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个referent就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weaklyreachable)。
WeakReference的referent是在构造时设置的,在没有被清除之前,可以用get()获取它的值。如果弱引用被清除了(不管是referent已经被垃圾收集了,还是有人调用了WeakReference.clear()),get()会返回null。相应地,在使用其结果之前,应当总是检查get()是否返回一个非null值,因为referent最终总是会被垃圾收集的。
用一个普通的(强)引用拷贝一个对象引用时,限制referent的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展referent的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。
弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合——这就是SocketManager类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap也被添加到JDK1.2的类库中,它对键(而不是对值)使用弱引用。如果在一个普通HashMap中用一个对象作为键,那么这个对象在映射从Map中删除之前不能被回收,WeakHashMap使您可以用一个对象作为Map键,同时不会阻止这个对象被垃圾收集。清单5给出了WeakHashMap的get()方法的一种可能实现,它展示了弱引用的使用:
清单5.WeakReference.get()的一种可能实现
publicclassWeakHashMap<K,V>implementsMap<K,V>...{
![]()
privatestaticclassEntry<K,V>extendsWeakReference<K>
implementsMap.Entry<K,V>...{
privateVvalue;
privatefinalinthash;
privateEntry<K,V>next;
...
}
![]()
publicVget(Objectkey)...{
inthash=getHash(key);
Entry<K,V>e=getChain(hash);
while(e!=null)...{
KeKey=e.get();
if(e.hash==hash&&(key==eKey||key.equals(eKey)))
returne.value;
e=e.next;
}
returnnull;
}
调用WeakReference.get()时,它返回一个对referent的强引用(如果它仍然存活的话),因此不需要担心映射在while循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap的实现展示了弱引用的一种常见用法——一些内部对象扩展WeakReference。其原因在下面一节讨论引用队列时会得到解释。
在向WeakHashMap中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get()返回null,这使得测试get()的返回值是否为null变得比平时更重要了。
用WeakHashMap堵住泄漏
在SocketManager中防止泄漏很容易,只要用WeakHashMap代替HashMap就行了,如清单6所示。(如果SocketManager需要线程安全,那么可以用Collections.synchronizedMap()包装WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的HashMap作为Map的实现。
清单6.用WeakHashMap修复SocketManager
publicclassSocketManager...{
privateMap<Socket,User>m=newWeakHashMap<Socket,User>();
![]()
publicvoidsetUser(Sockets,Useru)...{
m.put(s,u);
}
publicUsergetUser(Sockets)...{
returnm.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、timedblocking和untimedblocking。)
WeakHashMap有一个名为expungeStaleEntries()的私有方法,大多数Map操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。清单7展示了expungeStaleEntries()的一种可能实现。用于存储键-值映射的Entry类型扩展了WeakReference,因此当expungeStaleEntries()要求下一个失效的弱引用时,它得到一个Entry。用引用队列代替定期扫描内容的方法来清理Map更有效,因为清理过程不会触及活的项,只有在有实际加入队列的引用时它才工作。
清单7.WeakHashMap.expungeStaleEntries()的可能实现
privatevoidexpungeStaleEntries()...{
Entry<K,V>e;
while((e=(Entry<K,V>)queue.poll())!=null)...{
inthash=e.hash;
![]()
Entry<K,V>prev=getChain(hash);
Entry<K,V>cur=prev;
while(cur!=null)...{
Entry<K,V>next=cur.next;
if(cur==e)...{
if(prev==e)
setChain(hash,next);
else
prev.next=next;
break;
}
prev=cur;
cur=next;
}
}
}
结束语
弱引用和弱集合是对堆进行管理的强大工具,使得应用程序可以使用更复杂的可及性方案,而不只是由普通(强)引用所提供的“要么全部要么没有”可及性。下个月,我们将分析与弱引用有关的软引用,将分析在使用弱引用和软引用时,垃圾收集器的行为。
