文章目录
  1. 1. JVM的OOM异常简介
    1. 1.1. java.lang.OutOfMemoryError: PermGen space
    2. 1.2. java.lang.OutOfMemoryError:heap space或其它OutOfMemoryError
    3. 1.3. java.lang.OutOfMemoryError: GC overhead limit exceeded
    4. 1.4. java.lang.StackOverflowError
  2. 2. 写一个导致PermGen的OOM的Java程序
  3. 3. 防止递归的stackoverflow
    1. 3.1. 尾递归实现
    2. 3.2. 循环实现
    3. 3.3. 手工维护栈
    4. 3.4. 调整-XX:ThreadStackSize参数

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,如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package problem1;
import java.util.List;
import java.util.ArrayList;
/*
* VM Args as follows
* -XX:PermSize=2M -XX:MaxPermSize=2M
* -XX:MetaspaceSize=2M -XX:MaxMetaspaceSize=2M
*/
public class Problem1 {
public static void main(String args[]){
List<String> list = new ArrayList<String>();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}

程序运行结果如下图,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。代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package problem1;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.MethodInterceptor;
/*
* VM Args as follows
* -XX:PermSize=2M -XX:MaxPermSize=2M
* -XX:MetaspaceSize=2M -XX:MaxMetaspaceSize=2M
*/
public class Problem1_2 {
public static void main(String args[]){
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor(){
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable{
return proxy.invokeSuper(obj, args);
}
});
}
}
static class OOMObject{
}
}

程序运行结果如下图,jdk版本1.7.0_67。

如果是使用Java 8的话,也能看到和之前类似的结果,如下图。

防止递归的stackoverflow

如果递归的深度很深,也会引发stackoverflow。例如在Fibonacci数列的计算中,如果不做任何优化,很可能算不对1000位就stackoverflow了,即使能算完的,性能也不是很好,优化的办法有以下几种。本文的代码参考了这2篇文章5 6,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* java version "1.6.0_17"<br>
* 尾递归与迭代实现具有相当的性能;<br>
* 缓存实现的性能略高于非尾递归实现;<br>
* 递归:recursive 尾递归:tail recursive;<br>
* 尾递归时不需保存方法调用栈的参数及返回结果;于是申请的栈帧会被及时回收
*/
public class TestFibo {
public static void main(String[] args) {
int N=50;
long begin=System.currentTimeMillis();
System.out.println(fibo1(N)); //fibo1
System.out.println(System.currentTimeMillis()-begin);
begin=System.currentTimeMillis();
System.out.println(fibo2(N)); //fibo2
System.out.println(System.currentTimeMillis()-begin);
begin=System.currentTimeMillis();
System.out.println(fibo3(N)); //fibo3
System.out.println(System.currentTimeMillis()-begin);
begin=System.currentTimeMillis();
System.out.println(fibo4(N)); //fibo4
System.out.println(System.currentTimeMillis()-begin);
}
//1.1 非尾递归实现(书本上经常出现)
public static long fibo1(long n){
if(n<2) return n;
return fibo1(n-1)+fibo1(n-2); //小心栈溢出
}
//1.2 缓存实现(JS good part里有过介绍)
public static int LENGTH=30; //过大了就会占用过多空间
public static long[] cache=new long[LENGTH];
public static long fibo2(int n){
if(n<2) return n;
if(n>=LENGTH){
return fibo2(n-1)+fibo2(n-2);
}else if(cache[n]==0){
cache[n]=fibo2(n-1)+fibo2(n-2); //减少重复计算
}
return cache[n];
}
//1.3 迭代实现
public static long fibo3(long n){
if(n<2) return n;
long pre=1,prepre=1,ret=0;
for(int i=2;i<n;i++){
ret=pre+prepre;
prepre=pre;
pre=ret;
}
return ret;
}
//1.4 尾递归实现
public static long fibo4(int n){
if(n<2) return n;
return fibo4Helper(n, 1, 1, 3); //保持与非尾递归接口不变,是借助帮助方法实现尾递归的
}
private static long fibo4Helper(int n,long prepre,long pre,int begin){
if(n==begin) return pre+prepre;
return fibo4Helper(n, pre, prepre+pre, ++begin); //这里相当于迭代实现for-loop的浓缩
}
}

尾递归实现

尾递归是函数式编程(FP)中的概念,其主要思想是在函数递归调用时,函数调用栈的高度不增高,即,递归调用完成后,不需要再做其他的指令即可退出函数的情况,如上述代码的1.4部分。多数的函数式编程语言,可以将尾递归编译成循环,这样就不会发生栈溢出,可惜Java编译器没有尾递归的优化,因此,必须手动转换成循环实现。

循环实现

循环实现如上述代码1.3的部分,其实循环的思想还是来自于尾递归,如果一步不能想到递归转换为循环的方法,可以先转成为尾递归,再转循环,就简单多了。

手工维护栈

如果不能想到转换为尾递归的方法,那就只好手动去维护递归调用的栈,将所需要的数据存在栈中,然后对于手工栈非空的情况下,循环进行计算。以上的1.2缓存实现是一种优化了的手工栈,在这种实现中避免了重复计算。但是手工栈的问题是,需要有大量的空间消耗,以及手工栈维护的复杂性,这些都是无法避免的。其实,手工栈方法就是将函数调用的stack转移到堆上,堆的空间比较大,因此发生overflow的可能性就被降低了。

调整-XX:ThreadStackSize参数

如果不改变程序,可以调整-XX:ThreadStackSize参数,加大线程堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K,修改为8M或者更大,就可以支持更深的递归调用。也可以减小递归程序的局部变量表,比如,少用比较大的double,long类型,减少参数个数,局部变量注意作用域,如果可以在作用域外开,就不要开在作用域内,以此减少局部变量表的大小。

文章目录
  1. 1. JVM的OOM异常简介
    1. 1.1. java.lang.OutOfMemoryError: PermGen space
    2. 1.2. java.lang.OutOfMemoryError:heap space或其它OutOfMemoryError
    3. 1.3. java.lang.OutOfMemoryError: GC overhead limit exceeded
    4. 1.4. java.lang.StackOverflowError
  2. 2. 写一个导致PermGen的OOM的Java程序
  3. 3. 防止递归的stackoverflow
    1. 3.1. 尾递归实现
    2. 3.2. 循环实现
    3. 3.3. 手工维护栈
    4. 3.4. 调整-XX:ThreadStackSize参数

欢迎来到Valleylord的博客!

本博的文章尽量原创。