JVM内存溢出详解(栈溢出,堆溢出,持久代溢出、⽆法创建本地线程)
1、内存溢出和内存泄漏的区别
  内存溢出(Out Of Memory):是指程序在申请内存时,没有⾜够的内存空间供其使⽤,出现Out Of Memory。
  内存泄露(Memory Leak):是指程序在申请内存后,由于某种原因⽆法释放已申请的内存空间,导致这块内存⽆法再次被利⽤,造成系统内存的浪费。
  memory leak会最终会导致out of memory。
2、内存溢出分类
2.1 栈内存溢出(StackOverflowError):
  程序所要求的栈深度过⼤导致,可以写⼀个死递归程序触发。
2.2 堆内存溢出(OutOfMemoryError : java heap space)
需要分清是内存溢出还是内存泄漏:
(1)如果是内存溢出,则通过调⼤ -Xms,-Xmx参数。
(2)如果是内存泄露,则看对象如何被 GC Root 引⽤。
2.3 持久带内存溢出(OutOfMemoryError: PermGen space)
持久带中包含⽅法区,⽅法区包含常量池。
因此持久带溢出有可能是(1)运⾏时常量池溢出,也有可能是(2)⽅法区中保存的Class对象没有被及时回收掉或者Class信息占⽤的内存超过了我们配置。
⽤String.intern()触发常量池溢出。
Class对象未被释放,Class对象占⽤信息过多,有过多的Class对象。可以导致持久带内存溢出。
2.4 ⽆法创建本地线程
Caused by: java.lang.OutOfMemoryError:unable to create new native thread
系统内存的总容量不变,堆内存、⾮堆内存设置过⼤,会导致能给线程分配的内存不⾜。
3、内存溢出详解
3.1 栈溢出(StackOverflowError)
  栈溢出抛出 StackOverflowError 错误,出现此种情况是因为⽅法运⾏的时候栈的深度超过了虚拟机容许的最⼤深度所致。⼀般情况下是程序错误所致的,⽐如写了⼀个死递归,就有可能造成此种情
况。下⾯我们通过⼀段代码来模拟⼀下此种情况的内存溢出。
import java.util.*;
import java.lang.*;
public class OOMTest{
public void stackOverFlowMethod(){
stackOverFlowMethod();
}
public static void main(String[] args){
OOMTest oom = new OOMTest();
oom.stackOverFlowMethod();
}
}
运⾏上⾯的代码,会抛出如下的异常:
Exception in thread "main" java.lang.StackOverflowError
at OOMTest.stackOverFlowMethod(OOMTest.java:6)
jvm调优参数
对于栈内存溢出,根据《Java 虚拟机规范》中⽂版:
  如果线程请求的栈容量超过栈允许的最⼤容量的话,Java 虚拟机将抛出⼀个StackOverflow异常;如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是⽆法申请到⾜够的内存去完成
扩展,或者在新建⽴线程的时候没有⾜够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出⼀个OutOfMemory 异常。
3.2 堆溢出(OutOfMemoryError:java heap space)
堆内存溢出的时候,虚拟机会抛出 java.lang.OutOfMemoryError:java heap space。
出现此种情况的时候,我们需要根据内存溢出的时候产⽣的 dump ⽂件来具体分析(需要增加 -XX:+HeapDumpOnOutOfMemoryError jvm启动参数)。出现此种问题的时候有可能是内存泄漏,也有可
能是内存溢出了。
1、配置⽅法
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${⽬录}。
2、参数说明
(1)-XX:+HeapDumpOnOutOfMemoryError参数表⽰当JVM发⽣OOM时,⾃动⽣成DUMP⽂件。
(2)-XX:HeapDumpPath=${⽬录}参数表⽰⽣成DUMP⽂件的路径,也可以指定⽂件名称,例如:-XX:HeapDumpPath=${⽬录}/java_heapdump.hprof。如果不指定⽂件名,默认为:java_<pid><date><time>_heapDump.hprof 如果是内存泄漏,我们要出内存泄漏的对象是怎么被GC ROOT引⽤起来,然后通过引⽤链来具体分析泄露的原因。
如果出现了内存溢出问题,这往往是程序本⽣需要的内存⼤于了我们给虚拟机配置的内存,这种情况下,我们可以采⽤调⼤-Xmx来解决这种问题。
下⾯我们通过如下的代码来演⽰⼀下此种情况的溢出:
import 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
程序输出如下的信息:
[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操作也不会出现异常了。
  通过上⾯的实验其实也从侧⾯验证了⼀个结论:对象⼤于新⽣代剩余内存的时候,将直接放⼊⽼年代,当⽼年代剩余内存还是⽆法放下的时候,触发垃圾收集,收集后还是不能放下就会抛出内存溢
出异常了。
3.3 持久带溢出(OutOfMemoryError: PermGen space)
我们知道 Hotspot jvm 通过持久带实现了Java虚拟机规范中的⽅法区,⽽运⾏时的常量池就是保存在⽅法区中。
因此持久带溢出有可能是:
(1)运⾏时常量池溢出。
(2)⽅法区中保存Class对象没有被及时回收掉或者Class信息占⽤的内存超过了我们配置。
当持久带溢出的时候抛出 java.lang.OutOfMemoryError: PermGen space。可能在如下⼏种场景下出现:
使⽤⼀些应⽤服务器的热部署的时候,我们就会遇到热部署⼏次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的Class没有被卸载掉。
如果应⽤程序本⾝⽐较⼤,涉及的类库⽐较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)⽐较⼩的时候也可能出现此种问题。
⼀些第三⽅框架,⽐如spring、hibernate都是通过字节码⽣成技术(⽐如CGLib)来实现⼀些增强的功能,这种情况可能需要更⼤的⽅法区来存储动态⽣成的Class⽂件。
我们知道Java中字符串常量是放在常量池中的,String.intern()这个⽅法运⾏的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回常量池中对象的引⽤,不存在的话,先把此字符串
加⼊常量池,然后再返回字符串的引⽤。
那么我们就可以通过String.intern⽅法来模拟⼀下运⾏时常量区的溢出(JDK6下,因为jdk6中常量池位于PremGen区,jdk7之后将常量池移到了Java堆区)。下⾯我们通过如下的代码来模拟此种情况:
import 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
运⾏后的控制台输出如下图所⽰:
Exception 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通过持久带来实现⽅法区的说法。
3.4 ⽆法创建本地线程
java.lang.OutOfMemoryError: unable to create new native thread
最后我们在来看看java.lang.OutOfMemoryError:unable to create new native thread这种错误。出现这种情况的时候,⼀般是下⾯两种情况导致的:
程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过 ulimit -u 来查看此限制。
给虚拟机分配的内存过⼤,导致创建线程的时候需要的native内存太少。
建⽴每个线程,都需要给这个线程分配栈空间。
我们都知道操作系统对每个进程的内存是有限制的,我们启动jvm,相当于启动了⼀个进程,假如我们⼀个进程占⽤了4G的内存,那么通过下⾯的公式计算出来的剩余内存就是建⽴线程栈的时候可以⽤
的内存。
线程栈总可⽤内存 = 4G -(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占⽤的内存
通过上⾯的公式我们可以看出,-Xmx 和 MaxPermSize的值越⼤,那么留给线程栈可⽤的空间就越⼩,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越⼩。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增⼤进程所占⽤的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的⽬的。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。