Java常见内存溢出异常分析

嵌入式操作系统

57人已加入

描述

  Java 堆 OutOfMemoryError

  Java 堆是用来存储对象实例的, 因此如果我们不断地创建对象, 并且保证 GC Root 和创建的对象之间有可达路径以免对象被垃圾回收, 那么当创建的对象过多时, 会导致 heap 内存不足, 进而引发 OutOfMemoryError 异常。

  public class OutOfMemoryErrorTest{

  public static void main (String[] args){

  List《Integer》 list = new ArryList《》();

  int i=0;

  while(true){

  list.add(i++);

  }}}

  上面是一个引发 OutOfMemoryError 异常的代码, 我们可以看到, 它就是通过不断地创建对象, 并将对象保存在 list 中防止其被垃圾回收, 因此当对象过多时, 就会使堆内存溢出。

  通过 java -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError 我们设置了堆内存为 10 兆, 并且使用参数 -XX:+HeapDumpOnOutOfMemoryError 让 JVM 在发生 OutOfMemoryError 异常时打印出当前的内存快照以便于后续分析。

  编译运行上述代码后, 会有如下输出:

  》》》java -Xms10m - Xms10m-XX:HeapDumpOnOutOfMemoryError com.test.OutOfMemoryErrorTest 16-10-12 10:28

  java.lang.OutOfMemoryError:Java heap space

  Dumping heap to java_pid1810.hprof.。。

  Heap dump file created [14212861 bytes in 0.128 secs]

  Exception in thread “main” java.lang.OutOfMemoryError:Java heap space

  at java.util.Arrays.copyof(Arrays.java:3210)

  at java.util.Arrays.copyof(Arrays.java:3181)

  堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。

  如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。

  如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。

  下面我们通过如下的代码来演示一下此种情况的溢出:

  [java] view plain copyimport java.util.*;

  import java.lang.*;

  public class OOMTest{

  public static void main(String.。。 args){

  List《byte[]》 buffer = new ArrayList《byte[]》();

  buffer.add(new byte[10*1024*1024]);

  }

  }

  我们通过如下的命令运行上面的代码:

  java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest

  程序输入如下的信息:

  [plain] view plain copy[GC 1180K-》366K(19456K), 0.0037311 secs]

  [Full GC 366K-》330K(19456K), 0.0098740 secs]

  [Full GC 330K-》292K(19456K), 0.0090244 secs]

  Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

  at OOMTest.main(OOMTest.java:7)

  从运行结果可以看出,JVM进行了一次Minor gc和两次的Major gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。

  Java 栈 StackOverflowError

  我们知道, JVM 的运行时数据区中有一个叫做 虚拟机栈 的内存区域, 此区域的作用是: 每个方法在执行时都会创建一个栈帧, 用于存储局部变量表, 操作数栈, 方法出口等信息。

  因此我们可以创建一个无限递归的递归调用, 当递归深度过大时, 就会耗尽栈空间, 进而导致了 StackOverflowError 异常。

  下面是具体的代码:

  public class OutOfMemoryErrorTest{

  public static void main (String [] srgs){

  stackOutOfMemoryError(1);

  )

  public static void stackOutOfMemoryError(int depth){

  depth++;

  stackOutOfMemoryError(depth);

  }}

  当编译运行上述的代码后, 会输出如下异常信息:

  Exception in thread “main” java.lang.StackOverflowError

  at com.test.OutOfMemoryErrorTest.stackOutOfMemoryError(OutOfMemoryErrorTest.java:27)

  栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。

  出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。 下面我们通过一段代码来模拟一下此种情况的内存溢出。

  [java] view plain copyimport java.util.*;

  import java.lang.*;

  public class OOMTest{

  public void stackOverFlowMethod(){

  stackOverFlowMethod();

  }

  public static void main(String.。。 args){

  OOMTest oom = new OOMTest();

  oom.stackOverFlowMethod();

  }

  }

  运行上面的代码,会抛出如下的异常:

  [plain] view plain copyException in thread “main” java.lang.StackOverflowError

  at OOMTest.stackOverFlowMethod(OOMTest.java:6)

  通过上面的实验其实也从侧面验证了一个结论:当对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,出发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了

  持久带溢出(OutOfMemoryError: PermGen space)

  我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space。

  我在工作可能在如下几种场景下出现此问题。

  使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。

  如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。

  一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。

  我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出。下面我们通过如下的代码来模拟此种情况:

  [java] view plain copyimport java.util.*;

  import java.lang.*;

  public class OOMTest{

  public static void main(String.。。 args){

  List《String》 list = new ArrayList《String》();

  while(true){

  list.add(UUID.randomUUID().toString().intern());

  }

  }

  }

  我们通过如下的命令运行上面代码:

  java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest

  运行后的输入如下图所示:

  [plain] view plain copyException in thread “main” java.lang.OutOfMemoryError: PermGen space

  at java.lang.String.intern(Native Method)

  at OOMTest.main(OOMTest.java:8)

  通过上面的代码,我们成功模拟了运行时常量池溢出的情况,从输出中的PermGen space可以看出确实是持久带发生了溢出,这也验证了,我们前面说的Hotspot jvm通过持久带来实现方法区的说法。

  OutOfMemoryError:unable to create native thread

  最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。 出现这种情况的时候,一般是下面两种情况导致的:

  程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。

  给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。 线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。

  JVM内存区域组成

  简单的说java中的堆和栈

  java把内存分两种:一种是栈内存,另一种是堆内存

  1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;

  2。堆内存用来存放由new创建的对象和数组

  在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理

  堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点就是要在运行时动态分配内存,存取速度较慢;

  栈的优势是存取速度比堆要快,缺点是存在栈中的数据大小与生存期必须是确定的无灵活性。

  java堆分为三个区:New、Old和Permanent

  GC有两个线程:

  新创建的对象被分配到New区,当该区被填满时会被GC辅助线程移到Old区,当Old区也填满了会触发GC主线程遍历堆内存里的所有对象。Old区的大小等于Xmx减去-Xmn

  java栈存放

  栈调整:参数有+UseDefaultStackSize -Xss256K,表示每个线程可申请256k的栈空间

  每个线程都有他自己的Stack

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分