Java之OutOfMemoryError简单分析
最近编码遇到了Java内存溢出的问题,所以就想顺便总结一下几种导致Java内存溢出的栗子,以及碰到Java内存溢出要如何去解决。
Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆容量限制之后就会产生内存溢出的异常。
下面通过一段代码进行堆内存溢出异常的测试。
1 public class HeapOOM{ 2 static class HeapOOM{ 3 } 4 public static void main(String[] args) 5 { 6 List<OOMObject> list = new ArrayList<OOMObject>(); 7 While(1) 8 { 9 list.add(new OOMObject()); 10 } 11 } 12 }
代码中对Java堆的限制大小是20M,不可扩展。具体设置是在VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError。
当有堆溢出异常时,在异常堆栈信息栏中会出现“java.lang.OutOfMemoryError”,然后会提示:“Java heap space”。
解决思路:
一般是通过内存映象分析工具对Dump出来的堆转储快照进行分析,重点是确认内存中对象是否是必要的,也就是先分清楚是Memory Leak(内存泄露)还是Memory OverFlow(内存溢出)。如果是内存泄露,可以通过工具检查泄露对象到GC Roots的引用链。然后就能进而定位出泄露代码的位置。
如果不是内存泄露,也就是说内存中的对象都存活。那就检查虚机的参数,与物理机进行对比,看看是否可以调大,从代码检查是否存在生命周期过长的对象,尝试减少程序运行期间内存的消耗。
虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机中并不区分虚拟机栈以及本地方法栈,因此虽然-Xoss(用于设置本地方法栈的大小)参数存在,但是实际上是无效的。栈容量只由-Xss参数设定。
下面是虚拟机和本地方法栈OOM的测试代码:
1 public class JavaVMStackSOF{ 2 private int stackLength = 1; 3 public void stackLeak() { 4 stackLength++; 5 stackLeak(); 6 } 7 public static void main(String[] args) { 8 JavaVMStackSOF com = new JavaVMStackSOF(); 9 try{ 10 com.stackLeak(); 11 } 12 catch (Throwable e){ 13 System.out.println("Stack Length" + com.stackLength); 14 throw e; 15 } 16 } 17 }
上述代码使用了-Xss=128k,减少了栈内存的容量,结果抛出StackOverflowError异常,异常出现时输出的堆栈深度相应的缩小。
定义了大量的本地变量,增大此方法帧中本地变量表的长度,结果抛出StackOverflowError异常时输出的堆栈深度相应缩小。也就是说在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,都会抛出StackOverflowError异常。
解决思路:
如果使用虚拟机默认参数,栈深度在大多数情况下达到1000-2000没问题,正常的方法调用(含递归),这个深度完全够用,如果是建立多线程导致内存溢出,在不能减少线程数情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
方法区以及运行时常量池溢出
由于运行时常量池是方法区一部分,所以就放在一起了。
下面上代码:
1 public class RuntimeConstantPoolOOM{ 2 public static void main(String[] args) { 3 List<String> list = new ArrayList<String>(); 4 int i =0; 5 while(1) 6 list.add(String.valueOf(i++).intern()); 7 } 8 }
设置VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M。String.intern()是一个Native方法,它的作用是:如果字符串常量池已经包含了等于此String对象的字符串,则返回代表池中这个字符串的String对象:否则,将此String包含的字符串添加到常量池中,并且返回此String的引用。从结果中可以看到,在OutOfMemoryError后跟随的提示信息是:PermGen Space,说明运行时常量池属于方法区一部分。
解决思路:
方法区溢出是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判断条件是比较苛刻的。在经常动态生成大量的Class应用中,需要特别注意类的回收状况,常见的场景有:大量jsp或动态生成jsp文件的应用、基于OSGI的应用以及使用GGLib字节码增强(当前许多主流框架Spring、Hibernate都会用到GGLib这类字节码技术)和动态语言。
本机直接内存溢出
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如不指定,默认与java堆最大值一样。
下面贴代码:
1 public class DirectMemroyOOM{ 2 3 public static final int _1MB = 1024 * 1024; 4 5 public static void main(String[] args) throws Exception{ 6 Field unsafeField = Unsafe.class.getDeclareFields()[0]; 7 unsafeField.setAccessible(true); 8 Unsafe unsafe = (Unsafe) unsafeField.get(null); 9 while (1) 10 unsafe.allocateMemory(_1MB); 11 } 12 }
指定VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M,代码越过了DirectByBuffer类,直接通过反射获取Unsafe实例进行内存分配,之所以这么做是因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但是抛出的异常并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请内存的方法是unsafe.allocateMemory()。
解决思路:
由于此种内存溢出一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑一下是不是本机内存直接溢出了。
以上就是有关java内存溢出相关的总结了,如果有不对的地方,欢迎指出,大家一起交流。
PS:本博客欢迎转发,但请注明博客地址及作者~
博客地址:http://www.cnblogs.com/voidy/
<。)#)))≦