JVM的OutOfMemoryException
更新日期:
JVM的OOM异常简介
JVM的OOM(OutOfMemory)异常一般有以下几种,转载自1。
- java.lang.StackOverflowError:(很少)
- java.lang.OutOfMemoryError:heap space(比较常见)
- java.lang.OutOfMemoryError: PermGen space (经常出现)
- java.lang.OutOfMemoryError: GC overhead limit exceeded(某项操作使用大量内存时发生)
以下分别解释一下,从最常见的开始:
java.lang.OutOfMemoryError: PermGen space
这个异常比较常见,是说JVM里的Perm内 存区的异常溢出,由于JVM在默认的情况下,Perm默认为64M,而很多程序需要大量的Perm区内 存,尤其使用到像Spring等框架的时候,由于需要使用到动态生成类,而这些类不能被GC自动释放,所以导致OutOfMemoryError: PermGen space异常。解决方法很简单,增大JVM的 -XX:MaxPermSize 启动参数,就可以解决这个问题,如过使用的是默认变量通常是64M[5.0 and newer: 64 bit VMs are scaled 30% larger; 1.4 amd64: 96m; 1.3.1 -client: 32m.],改成128M就可以了,-XX:MaxPermSize=128m。如果已经是128m(Eclipse已经是128m了),就改成 256m。我一般在服务器上为安全起见,改成256m。
java.lang.OutOfMemoryError:heap space或其它OutOfMemoryError
这个异常实际上跟上面的异常是一个异常,但解决方法不同,所以分开来写。上面那个异常是因为JVM的perm区内 存区分少了引起的(JVM的内 存区分为 young,old,perm三种)。而这个异常是因为JVM堆内 存或者说总体分少了。解决方法是更改 -Xms -Xmx 启动参数,通常是扩大1倍。xms是管理启动时最小内 存量的,xmx是管里JVM最大的内 存量的。 注:OutOfMemoryError可能有很多种原因,根据JVM Specification, 可能有一下几种情况,我先简单列出。stack:stack分区不能动态扩展,或不足以生成新的线程。Heap:需要更多的内 存,而不能获得。Method Area :如果不能满足分配需求。runtime constant pool(从Method Area分配内 存)不足以创建class or interface。native method stacks不能够动态扩展,或生成新的本地线程。
java.lang.OutOfMemoryError: GC overhead limit exceeded
这个是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。我在JSP导大Excel的时候碰到过。最终解决方案是,关闭该功能,使用—— -XX:-UseGCOverheadLimit
java.lang.StackOverflowError
老实说这个异常我也没碰见过,但JVM Specification就提一下,规范上说有一下几种境况可能抛出这个异常,一个是Stacks里的线程超过允许的时候,另一个是当native method要求更大的内 存,而超过native method允许的内 存的时候。根据SUN的文档,提高-XX:ThreadStackSize=512的值。
总的来说调优JVM的内存,主要目的就是在使用内 存尽可能小的,使程序运行正常,不抛出内 纯溢出的bug。而且要调好最小内 存,最大内 存的比,避免GC时浪费太多时间,尤其是要尽量避免FULL GC。
补充:由于JDK1.4新增了nio,而nio的buffer分配内存比较特殊(读写流可以共享内存)。如果有大量数据交互,也可能导致java.lang.OutOfMemoryError。相应的JDK新增了一个特殊的参数:-XX:MaxDirectMemorySize 默认是64M,可以改大些如128M。
写一个导致PermGen的OOM的Java程序
此程序主要参考了2。为了使程序能更快的出现OOM,运行时加入了虚拟机参数-XX:PermSize=2M -XX:MaxPermSize=2M
,如下,
程序运行结果如下图,jdk版本1.7.0_67。
如果是在Java 8下运行这个程序,就要使用其他的参数,因为Java 8的JVM没有Perm区了,而改为了MetaSpace,参考3。所以要使用参数-XX:MetaspaceSize=2M -XX:MaxMetaspaceSize=2M
,如果还同时使用之前的参数会看到运行时会提示Java 8中已经移除这些参数了。其运行结果如下图,jdk版本1.8.0_11。
另外,由于Perm区存放了类的信息、类的静态变量、类中final类型的变量、类的方法信息,如果类的信息或静态变量过多,也会出现OOM的情况。不过Java对动态生成类的信息支持的不是很好,做起来比较麻烦。普通的反射机制,只能获取已有的类,并对这个类做一些修改,然后实例化。如果要动态创建一个Java类,就需要动态的compile,可以参考4。如果我们使用其中的方法,动态的生成类的信息,并实例化后放在List中,应该也能引发Perm区的OOM。以下这段代码参考《深入理解Java虚拟机 JVM高级特性与最佳实践》这本书,通过CGlib来动态生成类信息,最终引发Perm区的OOM。代码如下,
程序运行结果如下图,jdk版本1.7.0_67。
如果是使用Java 8的话,也能看到和之前类似的结果,如下图。
防止递归的stackoverflow
如果递归的深度很深,也会引发stackoverflow。例如在Fibonacci数列的计算中,如果不做任何优化,很可能算不对1000位就stackoverflow了,即使能算完的,性能也不是很好,优化的办法有以下几种。本文的代码参考了这2篇文章5 6,代码如下。
尾递归实现
尾递归是函数式编程(FP)中的概念,其主要思想是在函数递归调用时,函数调用栈的高度不增高,即,递归调用完成后,不需要再做其他的指令即可退出函数的情况,如上述代码的1.4部分。多数的函数式编程语言,可以将尾递归编译成循环,这样就不会发生栈溢出,可惜Java编译器没有尾递归的优化,因此,必须手动转换成循环实现。
循环实现
循环实现如上述代码1.3的部分,其实循环的思想还是来自于尾递归,如果一步不能想到递归转换为循环的方法,可以先转成为尾递归,再转循环,就简单多了。
手工维护栈
如果不能想到转换为尾递归的方法,那就只好手动去维护递归调用的栈,将所需要的数据存在栈中,然后对于手工栈非空的情况下,循环进行计算。以上的1.2缓存实现是一种优化了的手工栈,在这种实现中避免了重复计算。但是手工栈的问题是,需要有大量的空间消耗,以及手工栈维护的复杂性,这些都是无法避免的。其实,手工栈方法就是将函数调用的stack转移到堆上,堆的空间比较大,因此发生overflow的可能性就被降低了。
调整-XX:ThreadStackSize参数
如果不改变程序,可以调整-XX:ThreadStackSize参数,加大线程堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K,修改为8M或者更大,就可以支持更深的递归调用。也可以减小递归程序的局部变量表,比如,少用比较大的double,long类型,减少参数个数,局部变量注意作用域,如果可以在作用域外开,就不要开在作用域内,以此减少局部变量表的大小。