JVM运行过程中,程序不断的申请内存空间用于保存运行时数据,当程序申请的内存空间系统无法满足时,就会抛出内存溢出错误。内存溢出发生的区域以及相应的解决方案都不相同,下面我们逐一分析内存溢出类型及解决方案。
JVM内存溢出分为两种情况,OutOfMemoryError和StackOverflowError。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
OutOfMemoryError是在程序无法申请到足够的内存的时候抛出的异常,导致OutOfMemoryError异常的常见原因有以下几种:
在不同的Web服务器或程序中,此错误常见的错误提示如下:
OOM错误发生的场景很多,比如下面这段代码,最终会发生OutOfMemoryError,为了能更快的出现错误,我们可以设置一下jvm中堆的最大值,设置jvm值的方法是通过-Xms(堆的最小值),-Xmx(堆的最大值)
public static void main(String[] args) {
List users = new ArrayList();
while (true) {
users.add(new UserBean());
}
}
StackOverflowError代表的是,当程序中栈深度所需空间大小,超过了虚拟机分配给线程的栈大小时就会出现此error。StackOverflowError发生于单个线程的栈大小无法满足程序所需的栈空间大小时。
java栈是java虚拟机的一个重要的组成部分,在栈里进行线程操作,存放方法参数等等。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。栈在初始化过后是有一定的大小的,也可通过jvm参数-Xss设置每个线程的堆栈大小。栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。
栈深度可理解为单个线程的堆栈空间最多能产生多少个栈帧,当堆栈总大小不变时,栈帧存储的信息越多,栈帧越大,每个线程堆栈深度越小。
以下代码将会报StackOverflowError:
public static void test(String str){
System.out.println(str);
test(str);
}
通常可以把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程为单位的。
从下图中看出方法区和堆用黄色标记,和其他三个区域的不同点就是,方法区和堆是线程共享的,所有的运行在jvm上的程序都能访问这两个区域,堆,方法区和虚拟机的生命周期一样,随着虚拟机的启动而存在,而栈和程序计数器是依赖用户线程的启动和结束而建立和销毁。
Program Counter Regster(程序计数器):每一个用户线程对应一个程序计数器,用来指示当前线程所执行字节码的行号。由程序计数器给文字码解释器提供下一条要执行的字节码的的位置。根据jvm规范,这个区域不会发生内存溢出。
Java stack(java 虚拟机栈):这个区域是最容易出现内存异常的区域,每一个线程对应生成一个线程栈,线程每执行一个方法的时候,都会创建一个栈帧,用来存放方法的局部变量表,操作树栈,动态连接,方法入口。jvm规范对这个区域定义了两种内存异常。
Native MethodStack(本地方法栈):和虚拟机栈一样,不同的是处理的对象不一样,虚拟机栈处理java的字节码,而本地栈则是处理的Native方法。其他方面一致。
Heap(堆):前面说了堆是所有线程都能访问的,随着虚拟机的启动而存在,这块区域很大,因为所有的线程都在这个区域保存实例化的对象,因为每一个类型中,每个接口实现类需要的内存不一样,一个方法内的多个分支需要的内存也不尽相同,我们只有在运行的时候才能知道要创建多少对象,需要分配多大的地址空间。GC关注的正是这样的部分内容,所以很多时候也将堆称为GC堆。堆中肯定不会抛出StackOverflowError类型的异常,所以只有OutOfMemoryError相关类型的异常。
Method Area(方法区):用于存放已被虚拟机加载的类信息,常量,静态方法,即使编译后的代码。由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。这个区域只能抛出OutOfMemoryError类型的错误,OutOfMemoryError: PermGen space。
在发生OOM后需要重点排查以下几点:
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理。
对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
解决方法: 手动设置MaxPermSize大小修改TOMCAT_HOME/bin/catalina.sh
JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m"
发生在堆内存上的内存溢出。原因可能有很多种,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。
解决方案:增加jvm的内存大小。其中"-Xms128M"为初始内存,"-Xmx256M"为最大内存。
-Xmx2048m -Xms2048m
但是,对于内存泄漏问题,无法通过设置启动参数的方式来解决,这种情况下增加堆内存大小只会延缓OOM的出现时间,治标不治本。也不推荐一开始就将堆内存大小设置的很大,这样会掩盖测试期间可能出现的问题,导致线上问题的出现。
对于这种情况,我们应该对程序中可能出现内存泄漏的地方进行优化。主要包括避免死循环,应该及时释放种资源:内存, 数据库的各种连接,防止一次载入太多的数据。导致java.lang.OutOfMemoryError的根本原因是程序不健壮。因此,从根本上解决Java内存溢出的唯一方法就是修改程序,及时地释放没用的对象,释放内存空间。遇到该错误的时候要仔细检查程序。