内存溢出OOM和内存泄露memory leak 小结

时间:2022-08-31 20:55:33

out of memory

内存溢出就是内存越界。内存越界有一种很常见的情况是调用栈溢出(即*),虽然这种情况可以看成是栈内存不足的一种体现。但内存溢出并不一定跟内存分配有什么关系,因为还有一种情况是缓冲区溢出。


内存泄露 memory leak

是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!


简单的理解完概念后,小小总结一点比较容易导致内存泄露的一些原因吧:

  • 类管理自己的内存
  • 缓存
  • 监听器和其他回调
  • 集合类中有对对象的引用,使用完成后未清空,使得JVM不能回收;
而内存溢出的常见原因如下(某些情况下亦是内存泄露的原因):
  1. 内存中加载的数据量过于庞大,如一次从数据库中取出过多数据;
  2. 集合类中有对对象的引用,使用完成后未清空,是的JVM不能回收(积少成多);
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的bug
  5. 启动参数内存值设定过小

对于内存溢出在测试时候可以重点排查以下内容:
  1. 检查数据库查询,是否有一次获得全部数据的查询。一般来说,一次取十万条记录到内存,就可能引起内存溢出。在上线前的测试中,数据库数据较少,不容易出问题,但有可能线上数据数据库数据多了,一次查询就可能引起内存溢出,因此对于数据库查询尽量采用分页的方式查询。
  2. 检查代码中是否有死循环或递归调用(代码走查时重点查看)。
  3. 检查是否有循环重复产生新对象实体!
  4. 检查List,Map等集合对象是否有使用完后,未清除的问题。List,Map等集合对象始终有对象的引用,使得对象不能被GC回收。
说到这了,再看看java运行时的内存是怎样的呢:

内存溢出OOM和内存泄露memory leak 小结

java内存结构:

运行的内存区域有5个部分,

  • Method Area 【方法区】,
  • Java stack 【java 虚拟机栈】,
  • Native MethodStack【本地方法栈】,
  • Heap【堆】,
  • Program Counter Register【程序计数器】

图中:黄色区域即Method Area方法区和Heap堆是线程共享的,运行在jvm上的程序都能访问这两个区域;堆,方法区和虚拟机的生命周期一样,随着虚拟机的启动而存在,而Java stack 栈和 Program Counter Register程序计数器是依赖用户线程的启动和结束而建立和销毁。

program counter regster 程序计数器,每一个用户线程对应一个程序计数器,用来指示当前线程所执行的字节码的行号。由程序计数器给文字码解释器提供下一条要执行的字节码的位置,根据JVM规范,在这个区域中不会抛出oom的异常。

java stack 虚拟机栈,这个区域是最容易出现内存异常的区域,每一个线程对应一个线程栈,线程每执行一个方法的时候,都会创建一个栈帧,用来存放方法的局部变量表,操作树栈,动态连接,方法入口这和C#不一样,在c#中CLR没有栈帧的概念,都在在线程栈中通过压栈和出栈的方式进行数据的保存。JVM规范对这个区域定义了两种内存异常,OutOfMemoryError,*Error


native methodstack 本地方法栈:和虚拟机栈一样,不同的是处理的对象不一样,虚拟机栈处理java字节码,而本地栈则是处理native方法,其他方面一致。


heap 堆:堆是所有线程都能访问的,随着虚拟机的启动而存在,这块区域很大,因为所有的线程都在这个区域保存实例化的对象,因为每一个类型中,每个接口实现类需要的内存不一样,一个方法的多个分支需要内存也不尽相同,我们只有在运行的时候才能知道要创建多少对象,需要分配多大的地址空间。GC关注的正是这样的部分内容,所以很多时候也将堆称为GC堆。堆中肯定不会抛出*Error类型的异常,所以只有oom相关类型的异常。


method area 方法区:用于存放已被虚拟机加载的类信息,常量,静态方法,即使编译后的代码,同样只能抛出oom相关类型的异常。


附:内存溢出后台日志:

java.lang.OutOfMemoryError: PermGen space 
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:23)
Exception in thread "main" [Full GCjava.lang.OutOfMemoryError: Java heap space 
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at oom.HeapOOM.main(HeapOOM.java:21)  
 
可想而知是在堆中出现的问题,如何重现,由于是在堆中出现这个异常,那么就要处理好,不能被垃圾回收器给回收了,设置一下jvm中堆的最大值(这样才能够更快的出现错误),设置jvm值的方法是通过-Xms(堆的最小值),-Xmx(堆的最大值)。                                                                                   

 
 
Exception in thread "main" java.lang.*Error
at java.nio.CharBuffer.arrayOffset(Unknown Source)
at sun.nio.cs.UTF_8.updatePositions(Unknown Source)
at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(Unknown Source)
at sun.nio.cs.UTF_8$Encoder.encodeLoop(Unknown Source)
at java.nio.charset.CharsetEncoder.encode(Unknown Source)
at sun.nio.cs.StreamEncoder.implWrite(Unknown Source)
at sun.nio.cs.StreamEncoder.write(Unknown Source)
at java.io.OutputStreamWriter.write(Unknown Source)
at java.io.BufferedWriter.flushBuffer(Unknown Source)
at java.io.PrintStream.write(Unknown Source)
at java.io.PrintStream.print(Unknown Source)
at java.io.PrintStream.println(Unknown Source)
关于perm的异常内容,我们需要的是设置方法区的大小,实现方式是通过设置-XX:PermSize和-XX:MaxPermSize参数,内容如下

错误:OutOfMemoryError*Error

OutOfMemory 是在程序无法申请到足够的内存时抛出的异常,而*Errow是线程申请的栈深度大于虚拟机所允许的深度抛出的异常



不健壮代码的特征及解决办法 
1 、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为 null ,暗示垃圾收集器来收集该对象,防止发生内存泄露。

对于仍然有指针指向的实例, jvm 就不会回收该资源 , 因为垃圾回收会将值为 null 的对象作为垃圾,提高 GC 回收机制效率;

2 、我们的程序里不可避免大量使用字符串处理,避免使用 String ,应大量使用 StringBuffer ,每一个 String 对象都得独立占用内存一块区域;

  1. String str = “aaa”;   
  2.   
  3. String str2 = “bbb”;   
  4.   
  5. String str3 = str + str2;// 假如执行此次之后 str ,str2 以后再不被调用 , 那它就会被放在内存中等待 Java 的 gc 去回收 , 程序内过多的出现这样的情况就会报上面的那个错误 , 建议在使用字符串时能使用 StringBuffer 就不要用 String, 这样可以省不少开销;  
  6. StringBuffer dsb = newString("gosb");
  7. dsb.append(true);//执行后 就是dsbgosb       

3 、尽量少用静态变量,因为静态变量是全局的, GC 不会回收的;

4 、避免集中创建对象尤其是大对象, JVM 会突然需要大量内存,这时必然会触发 GC 优化系统内存环境;显示的声明数组空间,而且申请数量还极大。

这是一个案例想定供大家警戒:

使用jspsmartUpload作文件上传,现在运行过程中经常出现java.outofMemoryError的错误,用top命令看看进程使用情况,发现内存不足2M,花了很长时间,发现是jspsmartupload的问题。把jspsmartupload组件的源码文件(class文件)反编译成Java文件,如梦方醒:

  1. m_totalBytes = m_request.getContentLength();        
  2. m_binArray = new byte[m_totalBytes];      

变量m_totalBytes表示用户上传的文件的总长度,这是一个很大的数。如果用这样大的数去声明一个byte数组,并给数组的每个元素分配内存空间,而且m_binArray数组不能马上被释放,JVM的垃圾回收确实有问题,导致的结果就是内存溢出。

jspsmartUpload为什么要这样作,有他的原因,根据RFC1867的http上传标准,得到一个文件流,并不知道文件流的长度。设计者如果想文件的长度,只有操作servletinputstream一次才知道,因为任何流都不知道大小。只有知道文件长度了,才可以限制用户上传文件的长度。为了省去这个麻烦,jspsmartUpload设计者直接在内存中打开文件,判断长度是否符合标准,符合就写到服务器的硬盘。这样产生内存溢出,这只是我的一个猜测而已。

所以编程的时候,不要在内存中申请大的空间,因为web服务器的内存有限,并且尽可能的使用流操作,例如

  1. byte[] mFileBody = new byte[512];   
  2.          Blob vField= rs.getBlob("FileBody");   
  3.       InputStream instream=vField.getBinaryStream();   
  4.       FileOutputStream fos=new FileOutputStream(saveFilePath+CFILENAME);   
  5.          int b;   
  6.                       while( (b =instream.read(mFileBody)) != -1){   
  7.                         fos.write(mFileBody,0,b);   
  8.                          }   
  9.         fos.close();   
  10.       instream.close();  

5 、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。

6 、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable , vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃

7 、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。


http://www.cnblogs.com/200911/p/3965108.html