课题背景:
内存溢出和泄露在日常项目的开发过程中,出问题导致系统奔溃并不算常见,但是一旦出现,总避免不了一阵头疼,此类问题确实难以发现并解决。笔者参考了几本资料做了以下的总结。
1. 首先先简单谈谈什么是内存泄露和内存溢出(这块纯粹是为了加深笔者的记忆,可以忽略):
内存泄露(Memory Leak):简单的说,就是存在无法释放但是无用的内存空间。通常在java中,此类空间会被GC自动回收,但是在GC回收的前提是,此空间不再存在引用。滥用static和singleton对象,就可能导致无用对象空间长期占用。
内存溢出(Memory Overflow):简单的说,就是内存空间耗尽,无法在申请到更多的空间。
2. 堆空间:
常用的堆空间配置(这些配置大家应该都熟悉,增加此部分也完全是为了加深笔者的记忆,可以忽略):
-Xms20m:堆的初始空间大小。
-Xmx20m:堆的可夸张空间容量大小。
另外再唠叨两个虚拟机配置:
-XX:+HeapDumpOnOutOfMemoryError 堆溢出时,生成dump分析文件。
-XX:+UseGCOverheadLimit 作为1.6以后的GC安全策略,默认被开启,用于禁止GC过程无限制的执行,如果过于频繁,就直接抛出OOM。
堆空间常见的几种异常:
java.lang.OutOfMemoryError: Java heap space,无法申请到更多的内存空间。
java.lang.OutOfMemoryError: GC overhead limit exceeded,1.6以后,开启UserGCOverheadLimit之后,当GC的频繁的发生会引发此类问题,一般的原因是GC频繁回收的都是小对象,而程序又在持续的申请堆空间。
分析过程(前提当然是准备好了dump文件):
① 通过对上面的堆异常的描述,能对问题做个分类,是因为内存空间被耗尽,还是频繁的发生GC回收。
② 识别发生了内存泄露还是内存溢出引发的系统奔溃(通常情况下,这是不够的,因为最终引发奔溃的未必是奔溃的根本原因),借助VisualVM和Eclipse Memory Analyzer(后面简称EMA),分辨出"哪些对象是必须存在的?"和"哪些对象无需存在的?"。发生内存泄露,那么就需要修改原有的设计,减少对象的生命周期。发生内存溢出,通常情况下堆堆空间肯定是最容易解决的方案,但是有些时候,堆了空间仍然会导致此类问题,也有可能引发其他的问题,毕竟内存容量毕竟是有限的。说起来简单,这个步骤估计是要不少的时间去分析。
3. 栈空间:
这部分空间又分为虚拟机栈和本地方法栈。
常用的虚拟机配置:
-Xss128k: 设置栈容量,此处的栈容量并不是总的栈空间大小,而是每个线程具备的容量大小。事实上JVM堆栈的容量设了下限,记不清了可能是104K,这点可以让读者自己去查下。笔者平时没有尝试去挑战JVM的习惯。
常见的异常:
java.lang.StackOverflowError: 分配好的栈容量被消耗殆尽,该线程无法获取到更多的栈空间的时候。
java.lang.OutOfMemoryError: Unale to create new native thread。
分析:
先分析栈的空间定义(32位window):
2G(32位window设置的上限) = Xmx + MaxPermSize(永久区容量,后面会介绍到) + 虚拟机进程本身消耗 + 程序计数器 + 线程数 * Xss + 本地直接内存。
如果没有考虑到总内存的限制,胡乱的设置空间,意外的内存溢出就可能发生。Xss设置的栈容量消耗完的时候,就会出现StackOverflowError,而一旦线程数上来了,总的栈容量超过了上限(线程数 * Xss),那么就会出现OMM,所以,出现StackOverflowError常见于递归,而OOM则是线程过多。一般来说出现StackOverflowError快速的解决办法是加大Xss,如果硬件允许的情况下可以迁移到64位的JVM,当然改建代码自然是最好的办法。出现OOM,则可以调整其他内存容量,使栈总容量上去,同样,允许情况下,降低线程数也是个不错的选择。
4. 直接内存(Direct Memory):
这部分空间也是经常被忽略的一层,在NIO有可能直接的操作这部分空间。
常用的配置:
-XX:HeapDumpOnOfMemoryError: 用于设置最大的直接内存空间,如果不指定,则默认与java最大堆容量大小一样(Xss)
常见的异常:
java.lang.OutOfMemoryError: 很多时候不会有提示的信息,当然不是说其他的空间都会提示,只是常不常见的问题。
java.lang.OutOfMemoryError: Direct buffer memory. 比较容易的看出是直接内存的空间不足。
分析:
① 出现此类问题的时候,堆转储文件是不会生成的(就是前面说的dump文件)。
② 此类问题一般出现在大量使用NIO的时候。
通常次累问题不容解决,除了修改直接内存的空间以外,减小其他的内存容量也是必要的。
4. 方法区内存:
相关配置:
-XX:PermSize=10m:永久区初始空间
-XX:MaxPermSize=10m:永久区最大可扩展空间
常见的异常:
java.lang.OutOfMemoryError:PermGen space
分析:
方法区用于存放Class的相关信息,常量,静态变量,即使编译后的代码,1.6及1.6以前jdk的字符串常量池。 所以程序中大量使用动态生成class对象(动态代理,cglib,反射,JSP,OSGI)的场景下,比较常见。当然1.6的jdk,大量使用字符串常量也有可能出现。出现这种问题,自然也是适当调整之前的公式还有改进代码实现。