2008年9月3日星期三

JVM_003:Java 内存泄漏(摘录+整理)

1. 首先要回答的是:有Java内存泄漏这回事儿吗?
答案是有,但这样回答并不准确。因为Java的内存泄漏和C++ 的内存泄漏是两回事:C++程序中的内存泄漏是没有任何引用指向该对象,导致无法访问这些对象,也就无法释放这些对象使用的内存。而Java程序中的内存泄漏是有引用指向该对象,(如果没有的话,将被GC回收),但已经不需要使用该对象了(但GC并不知道,GC只看对象是否有引用),久而久之,这样的对象多了,就会造成内存泄漏。说内存泄漏也许不准确,应该称之为“无意识的对象保留(unintentional object retention)”。
如果GC仍然忠实地为我们工作,那么究竟是谁造成了Java内存泄漏?答案是你,程序的开发者。 为了深刻理解“无意识的对象保留”,请看下面这个例子:
1.object aobj = new object ( ) ;   
2.object bobj = new object ( ) ;   
3.object cobj = new object ( ) ;   
4.aobj = bobj;   
5.aobj = cobj;   
6.cobj = null;   
7.aobj = null;   
问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?   
答:第7行。
行1-3分别创建了object类的三个对象:aobj,bobj,cobj  
行4:此时对象aobj的句柄指向bobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
行5:此时对象aobj的句柄指向cobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
行6:此时仍没有任何一个对象符合垃圾收集器的收集标准。
行7:对象cobj符合了垃圾收集器的收集标准,因为cobj的句柄指向单一的地址空间。在第6行的时候,cobj已经被赋值为null,但由cobj同时还指向了aobj(第5行),所以此时cobj并不符合垃圾收集器的收集标准。而在第7行,aobj所指向的地址空间也被赋予了空值null,这就说明了,由cobj所指向的地址空间已经被完全地赋予了空值。所以此时cobj最终符合了垃圾收集器的收集标准。 但对于aobj和bobj,仍然无法判断其是否符合收集标准。

2. 哪些情况可能会导致Java的内存泄漏?
(1)多线程:在多线程程序中,线程里保存的只是对象的引用,而对象的实例都保存在Heap中。当一个线程结束,线程里对象引用将被删除,但在其他线程中可能还有指向该对象的引用,那么这些对象就有可能成为Heap中的垃圾。
(2)全局集合:这种集合生命周期极长,向集合中加入对象后,忘了删除不用的对象。例子参见《Effective Java》第5条:消除过期的对象引用。
(3)缓存:缓存用于快速查找执行过的操作结果。通常算法如下:
  第一步,检查结果是否在缓存中,如果在,就返回结果。
  第二步,如果结果不在缓存中,就进行计算。
  第三步,将计算出来的结果添加到缓存中。以便以后对该操作的调用可以使用。
  问题就出现在第三步,能否保证没有其它不该加入缓存的对象被加入缓存了。为了预防这种具有潜在破坏性的设计,必须对缓存所使用的内存容量有一个上限。因此,更好的算法是:
  第一步,检查结果是否在缓存中,如果在,就返回结果。
  第二步,如果结果不在缓存中,就进行计算。
  第三步,如果缓存所占的空间过大,按照LRU算法移除缓存对象。
  第四步,将计算出来的结果添加到缓存中。
(4)ClassLoader:Java ClassLoader结构的使用为内存泄漏提供了许多可乘之机。正是该结构本身的复杂性使ClassLoader在内存泄漏方面存在如此多的问题。ClassLoader的特别之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如:字段、方法和类。这意味着只要有对字段、方法、类或 ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。因为ClassLoader本身可以关联许多类及其静态字段,所以就有许多内存被泄漏了。
(5)忘记释放某些资源,比如异常时没有加finally{}来释放数据库连接。
(6)给某对象加了listener,对象已经没用了,但listener还在。

3. 如何判断发生了Java的内存泄漏
(1)最不幸的信号:出现了OutOfMemoryError。
这通常发生在您最不愿意它发生的生产环境中,此时几乎不能进行调试。一般情况下,都是代码的问题。所以建议重要的应用一定要做内存泄漏测试。如果是因为测试环境运行应用程序的方式与生产系统不完全相同,因而导致泄漏只出现在生产环境中,在这种情况下,需要使用一些开销较低的工具来监控和查找内存泄漏。还需要能够无需重启系统或修改代码就可以将这些工具连接到正在运行的系统上。
如果没有任何工具,最土的办法就是使用 -verbose:gc参数,不间断地监控GC的活动,确定内存使用量是否随着时间增加。如果确实如此,就发生了内存泄漏。
注意,出现OutOfMemoryError,也有可能应用程序确实正在使用这么多的内存,此时必须增加Heap的大小。
(2)如果发生了内存泄漏,应该如何找到泄漏点呢?
Heap快照可以帮助我们找到泄漏点。操作步骤如下:
  第一步,启动并运行系统,直到系统达到一个稳定的状态。
  第二步,强制进行一次垃圾收集,并且对此时的Heap做一次快照。
  第三步,进行任何可能产生内存泄漏的操作。
  第四步,再强制进行一次垃圾收集,并且对此时的Heap做一次快照。
比较两次快照,看看哪些对象的被引用数量比第一次快照时增加了。因为在快照之前强制进行了垃圾收集,那么剩下的对象都应该是被应用程序所引用的对象,并且通过比较两次快照我们可以准确地找出那些被程序保留的、新产生的对象。然后再结合你对程序代码的了解,判断出哪些对象是被无意识保留的,跟踪这些对象的引用链,直到找出源头。

没有评论: