技术开发 频道

使用IBM静态工具优化Java代码(二):分析错误报告

  这是报出的ERROR23错误模式。内存泄漏的后果非常严重,即使每次运行只有少量内存泄漏,但是长期运行之后,系统仍然会面临彻底崩溃的危险。

  在 C/C++ 中,内存泄漏(Memory Leak)一直是程序员特别头疼的问题,因为它出错时的表现特征经常很不稳定(比如:错误表象处不唯一,出错频率不定等),而且出现问题的表象处经常与内存泄漏错误代码相隔甚远,所以很难被定位查出。在 Java 中,垃圾回收器 (Garbage Collection,GC) 的出现帮助程序员实现了自动管理内存的回收,所以很多程序员认为 Java 不存在内存泄漏问题,其实不然,垃圾回收器并不能解决所有的内存泄漏问题,所以 Java 也存在内存泄漏,只是表现与 C/C++ 不同。

  为什么 Java 会出现内存泄漏呢?因为垃圾回收器只回收那些不再被引用的对象。但是有些对象的的确确是被引用的(可达的),但是却无用的(程序以后不再使用这些对象),这时垃圾回收器不会回收这些对象,从而导致了内存泄漏,抛出异常 java.lang.OutOfMemoryError。以下是导致内存泄漏的常见的例子(其中某些例子 BEAM 很难查出,这里列出只是为了给读者提供一个反例进行学习)。

  清单 13. 内存泄漏的 Hashtable

1 public class HashtableLeakDemo
2 {
3
4 static Hashtable<Integer, String> names = new Hashtable<Integer, String>();
5
6 void leakingHash( int num ) {
7      for( int i = 0; i < num; i++ ) {
8      names.put( new Integer(i) , "developerWorks");
9      }
10      // 接下来是继续对 names 哈希表进行的操作,但是忘了移除其中的表项
11 }
12 }
13

  leakingHash 会往 Hashtable 中不停地加入元素,但是却没有相应的移除动作(remove),而且 static 的 Hashtable 永远都会贮存在内存中,这样必将导致 Hashtable 越来越大,最终内存泄漏。

  清单 14. 内存泄漏的 Vector

1 public class VectorLeakDemo
2 {
3
4 static Vector<String> v = new Vector<String>();
5
6 void leakingVector( int num ) {
7      for( int i = 0; i < num; i++ ) {
8      v.add( String.valueOf(i) );
9      }
10      // 虽然进行了 remove,但是却没有移除干净
11      for( int i = num - 1; i > 0; i--  ) {
12      v.remove( i );
13      }
14 }
15 }

  每次调用 leakingVector 都会少 remove 一个 String 元素,如果 Vector 中的元素不是 String,而是数据库中一些非常大的记录(record),那么不停调用 leakingVector 将很快导致内存耗光。

  清单 15. 内存泄漏的 Buffer

1 public class BufferLeakDemo
2 {
3      private byte[] readBuffer;
4     
5      public void readFile( String fileName ) {
6           File f = new File( fileName );
7         
8           int len = (int)f.length();
9           //readBuffer 的长度只增不减
10           if ( readBuffer == null || readBuffer.length < len ) {
11                readBuffer = new byte[len];
12           }
13         
14           readFileIntoBuf( f, readBuffer );
15      }
16     
17      public void readFileIntoBuf( File f, byte[] buffer ) {
18           // 将文件内容读取到 buffer 中
19      }
20 }
21

  在BufferLeakDemo 对象的生命周期中,一直会有一个 readBuffer 存在,其长度等于读到的所有文件中最长文件的长度,而且更糟糕的是,该 readBuffer 只会增大,不会减小,所以如果不停的读大文件,就会很快导致内存泄漏。

  清单 16. 内存泄漏的 Stream 流 1

1 public void writeFile( String fileName ) {
2
3 OutputStream writer = null;
4
5 writer = new FileOutputStream(fileName);
6
7         // 接下来对 writer 进行操作,但是结束后忘记关闭 close
8 }
9

  文件输出流 FileOutputStream 使用完了没有关闭,导致 Stream 流相关的资源没有被释放,内存泄漏。

  清单 17. 内存泄漏的 Stream 流 2

1 public void writeFile( String srcFileName, String dstFileName )  {
2 try {
3      InputStream reader = new FileInputStream( srcFileName );
4      OutputStream writer = new FileOutputStream( dstFileName );
5             
6      byte[] buffer = new byte[1024];
7       
8         // 将源文件内容读入到 buffer 中    
9      reader.read(buffer);
10      // 将 buffer 中的数据写入到目的文件中
11      writer.write(buffer);
12             
13      reader.close();
14      writer.close();
15
16 } catch ( Exception e ) {
17        // 对异常情况进行处理    
18 }
19 }
20

  如果 reader 读取文件时 InputStream 发生异常,那么 writer 将不会被关闭,从而导致内存泄漏。

  总结:

  一些 Collection 类,如 Hashtable,HashSet,HashMap,Vector 和 ArrayList 等,程序员使用时一般容易忘记 remove 不再需要的项(如清单 13),或者虽然 remove,但是 remove 的不干净(如清单 14),这些都可能会导致无用的对象残留在系统中,这样的程序长时间运行,可能会导致内存泄漏。特别是当这些 Collection 类的对象被声明为 static 时或存活于整个程序生命周期时,就更容易导致内存泄漏。

  有些 buffer 在其生命周期中有时可能会很大,大到有可能导致内存泄漏(如清单 15)。

  使用 Stream 流时(如 FileOutputStream,PrintStream 等),创建并使用完毕后忘记关闭 close(如清单 16),或者因为异常情况使得关闭 Stream 流的 close 的语句没有被执行(如清单 17),这些都会导致 Stream 流相关的资源没有被释放,从而产生内存泄漏。

  建议的解决方法:

  程序员编码时注意手动释放一些已经明确知道不再使用的对象。最简单的方法就是将其置为 null,告诉垃圾回收器你已经不再引用他们,从而垃圾回收器可以替你回收这些对象所占用的内存空间。

  使用 Collection 类对象时(如 Hashtable,HashSet,HashMap,Vector 和 ArrayList 等),如果可以,尽量定义其为局部变量,减少外界对其的引用,增大垃圾回收器回收他们的可能性。

  使用 Collection 类对象时(如 Hashtable,HashSet,HashMap,Vector 和 ArrayList 等),注意手动 remove 其中不再使用的元素,减少垃圾对象的残留。

  使用事件监听器时(event listener),记住将不再需要监听的对象从监听列表中解除(remove)。

  使用 Stream 流时,一定要注意创建成功的所有 Stream 流一定要在使用完毕后 close 关闭,否则资源无法被释放。

  在 try / catch 语句中,添加 finally 声明,对 try 中某些可能因为异常而没释放的资源进行释放。

  在 class 中添加 finalize() 方法,手动对某些资源进行垃圾回收。

  可以使用一些可以检测内存泄漏的工具,如 Optimizeit Profiler,JProbe Profiler,JinSight, Rational 公司的 Purify 等,来帮助找出代码中内存泄漏的错误。

  其它技巧

  使用 Iterator 时,一定要先调用 hasNext() 后,再调用 next(),而且不要在一个 Iterator 的 hasNext() 成功后,去调用另外一个 Iterator 的 next(),如清单 18 。

  清单 18. 使用 Iterator 出错

1 Iterator firstnames = ( new Vector() ).iterator();
2 Iterator lastnames =  ( new Vector() ).iterator();
3     
4 while ( firstnames.hasNext() ) {
5     //firstnames 中存在下一个元素,但 lastnames 可能已经没有元素了
6 String name = firstnames.next() + "." + lastnames.next();    
7 }
8

  注意 switch 语句中是否缺少 break 。有的时候程序员有意让多个 case 语句在一次执行,但是有的时候却是忘写 break,导致发生了意想不到的结果,如清单 19 。

  清单 19. switch 语句中缺少 break

1 switch ( A )
2 {
3     // 程序员原本的意思是 A 为 0 时,B 为 0,A 为 1 时,B 为 1,其实 B 永远都不可能为 0
4 case 0: B = 0;
5 case 1: B = 1; break;
6 }
7

  注意避免恒正确或恒错误的条件,如清单 20 。

  清单 20. 常见的恒正确或恒错误的条件

1 1
2 if ( S.length() >= 0 ) // S 是 String 对象,它的长度永远大于等于 0,条件恒正确
3 2
4 // 程序员本来的意图是想介于 MIN 和 MAX 之间的值才成立,却误将” && ”写成” || ”,导致条件恒成立
5 if ( x >= MIN || x <= MAX )
6 3
7 final boolean singleConnection = true;
8 // final 型的 singleConnection 永远为 true,所以该条件恒成立,而且 connect() 永远不会被执行
9 if ( singleConnection || connect() )
10

  注意在 if 语句中是否少了 else 分支,如清单 21。

  清单 21. if 语句中少了 else 分支

1 if ( S == “ d ” ) { … }
2 else if ( S == “ e ” ) { … }
3 else if ( S == “ v ” ) { … }
4 else if ( S == “ e ” ) { … }
5 // 少了 else 语句,漏掉的情况可能会产生异常,应该加上 else 语句对剩下的条件进行判断和处理

  switch 语句最好对所有的 case 进行判断,并且不要忘记对 default 情况进行处理。

  结束语

  本文通过简单易懂的实例介绍了 Java 编码中程序员容易犯的一些典型错误,并给出了分析和解决方法,告诉读者如何才能写出高质量 Java 代码。

0
相关文章