java中OOM错误浅析 --(认为面试可以聊的东西)
嗯,生活加油鸭。。。。 实习中遇到OOM错误
GC overhead limit exceeded 问题,所以整理一下OOM异常问题:不对的地方请小伙伴留言^_^
先看一下“阿里的开发手册”对OOM的描述:
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
- 内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
- 内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
然后看一下《深入理解java虚拟机》中描述的OOM发生区域:
- 1)Java虚拟机栈:
描述Java方法执行的内存模型,与计数器一般,java虚拟机栈也是线程私有的,它的生命周期与线程相同,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链表,方法出口等信息。每个方法从调用到执行完成过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
-
- 局部变量表:存储了编译期可知的各种基本数据类型,对象的引用(reference类型,即指向对象起始地址的引用指针)和returnAddress类型(指向一条字节码指令的地址)。long与double类型数据会占用2个局部变量空间,其余的数据类型会占用一个,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,方法在帧中分配的局部变量空间是完全确定的。在方法运行期不会改变局部变量表的大小。
在Java虚拟机规范中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
- 2)本地方法栈:
与虚拟机类似,区别是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。也会抛出StackOverflowError异常,OutOfMemoryError异常
- 3)Java堆:
java 堆是虚拟机所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一作用是用来存放对象实例,几乎所有的对象实例以及数组都要在堆上分配地址。
java堆是垃圾收集器管理的主要区域。也被称为GC堆。Java堆可以处于物理上不连续的内存空间,只用逻辑上连续即可,可以通过-Xmx和Xms控制。如果在堆中没有内存完成实例分配,并且堆中也无法扩展,会抛出OutOfMenory异常。
- 4)运行时常量池:
是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池对于Class文件常量池的一个特征是具备动态性,Java语言不要求常量一定是在编译期才产生,即并非预置入Class文件中的常量池的内容可以才能进入方法区运行时常量池,运行期也可以将新的常量放入池中,比如String的intern()方法。当常量池无法 在申请内存时会抛出OutOfMethodError异常。
之后看具体的实例:
一、Java堆溢出:
package com.company; import java.util.ArrayList; import java.util.List; /** * @Author: Liruilong * @Date: 2019/7/13 10:03 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args){ List<OOMObject> list = new ArrayList<>(); while (true){ list.add(new OOMObject()); } } }
java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid13048.hprof ...
Heap dump file created [29202866 bytes in 0.128 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3204)
at java.util.Arrays.copyOf(Arrays.java:3175)
at java.util.ArrayList.grow(ArrayList.java:246)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:220)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:212)
at java.util.ArrayList.add(ArrayList.java:443)
at com.company.HeapOOM.main(HeapOOM.java:19)
二、虚拟机栈和本地方法栈溢出:
package com.company; /** * @Author: Liruilong * @Date: 2019/7/13 18:00 * @VM Args: -Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeng(){ stackLength++; stackLeng(); } public static void main(String[] args)throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeng(); }catch (Throwable throwable){ System.out.println("sstack length:"+oom.stackLength); throw throwable; } } }
sstack length:1001 Exception in thread "main" java.lang.StackOverflowError at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:13) at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:14) ………………
三、方法区和运行时常量池溢出
String.intern()是一个Native方法,当字符串对象已经包含一个String的字符串引用时,则返回字符串的引用,反之,将字符串添加到常量池中,并发挥Sting对象的引用。
在JDK1.6之前的版本,由于常量池分配永久代中,所以可以通过限制方法区的大小来限制常量池容量。
package com.company; import java.util.ArrayList; import java.util.List; /** * @Author: Liruilong * @Date: 2019/7/14 7:05 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class RuntimConstantPoolOOM { public static void main(String[] args) { // 使用List保持常量池的引用,避免Full GC回收常量池行为 List<String> list = new ArrayList<>(); int i = 0; while (true){ list.add(String.valueOf(i++).intern()); } } }
最后看遇到的问题:
查了一下:
oracle官方给出了这个错误产生的原因和解决方法:
Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded
Cause: The detail message "GC overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations.
Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.
原因:
大概意思就是说,JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存,频繁的进行内存回收(最起码已经进行了5次连续的垃圾回收),JVM就会曝出ava.lang.OutOfMemoryError: GC overhead limit exceeded错误。
即 超出了GC开销限制。是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存。
经过分析,我使用多线程的解析文件,解析的文件信息存放占用很大的内存,而使用时,我使用其中一小部分,但是整个内存被占用着,GC无法进行回收,所以会报错。我解决的问题是把需要的信息提取出来放到内存里。减少了对内存的使用。
解决方法:
- 1、增加heap堆内存。
- 增大堆内存 set JAVA_OPTS=-server -Xms512m -Xmx1024m -XX:MaxNewSize=1024m -XX:MaxPermSize=1024m
- 2、增加对内存后错误依旧,获取heap内存快照,使用Eclipse MAT工具,找出内存泄露发生的原因并进行修复。
- 3、优化代码以使用更少的内存或重用对象,而不是创建新的对象,从而减少垃圾收集器运行的次数。如果代码中创建了许多临时对象(例如在循环中),应该尝试重用它们。
- 4、除了使用命令-xms1g -xmx2g设置堆内存之外,尝试在启动脚本中加入配置:
在启动脚本中添加-XX:-UseGCOverheadLimit
命令。这个方法只会把“java.lang.OutOfMemoryError: GC overhead limit exceeded”变成更常见的java.lang.OutOfMemoryError: Java heap space错误。