Java JVM:内存溢出(栈溢出,堆溢出,持久代溢出以及 nable to create native thread)
转载链接:http://www.cnblogs.com/lgqboke/p/5809559.html、http://www.cnblogs.com/lgqboke/p/5809589.html
包括:
1. 栈溢出(*Error)
2. 堆溢出(OutOfMemoryError:java heap space)
3. 永久代溢出(OutOfMemoryError: PermGen space)
4. OutOfMemoryError:unable to create native thread
Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了两部:新生代,老年代。在堆内存之外,还有永久代,其中永久代实现了规范中规定的方法区,而内存模型中不同的部分都会出现相应的OOM错误,接下来我们就分开来讨论一下。
栈溢出(*Error)
栈溢出抛出*Error错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。 下面我们通过一段代码来模拟一下此种情况的内存溢出。
import java.util.*;
import java.lang.*;
public class OOMTest{
public void *Method(){
*Method();
}
public static void main(String... args){
OOMTest oom = new OOMTest();
oom.*Method();
}
}
运行上面的代码,会抛出如下的异常:
Exception in thread "main" java.lang.*Error
at OOMTest.*Method(OOMTest.java:6)
对于栈内存溢出,根据《Java 虚拟机规范》中文版:如果线程请求的栈容量超过栈允许的最大容量的话,Java 虚拟机将抛出一个*异常;如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。
堆溢出(OutOfMemoryError:java heap space)
堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。
- 如果内存泄露,我们要找出泄露的对象是怎么被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操作也不会出现异常了。
通过上面的实验其实也从侧面验证了一个结论:当对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,触发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了。
持久代溢出(OutOfMemoryError: PermGen space)
持久代和类加载器
Java对象是java 类的实例。每当创建一个Java对象时,Java虚拟机都会创建该对象的内部引用并且保存在堆中。如果一个类是第一次访问,那么它必须通过Java虚拟机加载进来。类加载是定位,寻找,加载class文件和解析class文件结构的过程。类加载器负责确保加载正确的class文件。Java程序里面每一个class文件需要被同一个类加载加载。类加载器是 java.lang.ClassLoader 类的实例。目前为止,类加载器是在持久代空间里面加载类的。
Java虚拟机也创建了Java类的内部引用保存在持久代。在垃圾回收期间,Java对象和类都当做对象处理并且以同样的方式回收。最初Java对象和类都是保存在堆中。
一个性能优化措施:一旦持久代创建后,就会把classes放入里面。Classes是Java虚拟机的部分实现,我们不应该用我们的数据结构填满Java堆。持久代包含以下类信息:
- 类方法
- 类名称
- 常量池信息
- 对象数组和与类相关的类型数组
- 被Java虚拟机使用的内部对象
- 编译器用于优化的信息
现在我们知道了持久代是什么,接下来看看在这块会是什么原因引起内存问题。
持久代空间
当Java虚拟机需要加载定义的一个新class,但是在持久代中没有足够的空间就会抛出‘Java.Lang.OutOfMemoryError: PermGen Space’异常。默认分配给持久代的大小在server模式下是64MB ,在client模式下是32MB 。这就有两个原因可能会引起持久代内存溢出问题的发生。
第一个原因可能是你应用或者服务器已经有非常多的class在你的持久代中,已经不能容纳所有的class了。
-XX:MaxPermSize=XXXM
如果是因为大量的class导致持久代的空间的不足引起的问题,那么你可以增加持久代的大小通过–XX:MaxPermSize=XXm 参数。这将增加持久代的可用空间来保存class。看起来像这样: -XX:MaxPermSize=256m
-XX:+CMSClassUnloadingEnabled
这个参数表示在使用CMS垃圾回收机制的时候是否启用类卸载功能。默认这个是设置为不启用的,所以你想启用这个功能你需要在Java参数中明确的设置下面的参数:
-XX:+CMSClassUnloadingEnabled
如果你启用了CMSClassUnloadingEnabled ,垃圾回收会清理持久代,移除不再使用的classes。这个参数只有在 UseConcMarkSweepGC 也启用的情况下才有用。参数如下:
-XX:+UseConcMarkSweepGC
-XX:+CMSPermGenSweepingEnabled
这个参数表示是否会清理持久代。默认是不清理的,因此我们需要明确设置这个参数来调试持久代内存溢出问题。这个参数在Java6中被移除了,因此你需要使用 -XX:+CMSClassUnloadingEnabled 如果你是使用Java6或者后面更高的版本。那么解决持久代内存大小问题的参数看起来会是下面这样子:
-XX:MaxPermSize=128m -XX:+UseConcMarkSweepGC XX:+CMSClassUnloadingEnabled
内存泄露
第二个原因可能是内存泄露。那么加载的类怎样变成不用的呢?
在Java中通常class是永久存在的。一旦class被加载,他们就呆在内存中,即使服务器上的应用停掉了。像cglib这样可以动态产生class的类库会使用很多持久代空间,因为它动态的创建很多class。频繁的使用在运行时创建的代理类。当一个类定义可以为多个实例重用时很容易创建新的代理类。
Sping和Hibernate经常会代理某些class。这些代理的class是通过类加载器加载的。产生的类定义如果一直不回收就会导致持久代空间很快就满了。
你需要确定是不是内存泄露引起的持久代空间的问题,同时解决它。增加持久代空间大小将不会有用,这只会延迟它的发生,因为在某个时间点持久代还是会被填满。
场景复现
我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space。可能在如下几种场景下出现:
- 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
- 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
- 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。
我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出.下面我们通过如下的代码来模拟此种情况:
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通过持久带来实现方法区的说法。
一旦你遇到了持久代内容溢出问题,你需要找出这个问题是因为加载了大量的class文件还是内存泄露引起的。如果是因为class的数量过多,你可以增加持久代分配的空间大小来解决这个问题。如果是因为内存泄露,你需要引起内存泄露的根源所在并且定位它。一些框架像cglib,Spring,Hibernate会创建大量的动态类,因此对于使用这些框架的应用最好是分配多一点的持久代空间。
OutOfMemoryError:unable to create native thread
最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。 出现这种情况的时候,一般是下面两种情况导致的:
1. 程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。
2. 给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。
我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存
通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。
总结:
- 栈内存溢出:程序所要求的栈深度过大导致。
- 堆内存溢出: 分清 内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用。不足则通过 调大 -Xms,-Xmx参数。
- 持久带内存溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。
- 无法创建本地线程:总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。