堆溢出、栈溢出、永久代溢出、直接内存溢出,无法创建本地线程的分析以及代码实现
-
栈溢出(*Error)
程序所要求的栈深度过大导致,可以写一个死递归程序触发。 - 堆溢出(OutOfMemoryError:Java heap space)
- 分清内存溢出还是内存泄漏
- 泄露则看对象如何被 GC Root 引用。
- 溢出则通过 调大 -Xms,-Xmx参数。
- 永久代溢出或叫持久代(OutOfMemoryError: PermGen space)
- 持久带中包含方法区,方法区包含常量池
- 因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置
- 用String.intern()触发常量池溢出
- Class对象未被释放,Class对象占用信息过多,有过多的Class对象。可以导致持久带内存溢出
-
直接内存溢出
-
无法创建本地线程(OutOfMemoryError:unable to create native thread)
总容量不变,堆内存,非堆内存设置过大,会导致能给线程的内存不足。
注意每一个方法的上面的虚拟机参数
一、堆溢出 创建对象时如果没有可以分配的堆内存,JVM就会抛出OutOfMemoryError:java heap space异常。
堆溢出实例:
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
int i=0;
while(true){
list.add(new byte[5*1024*1024]);
System.out.println("分配次数:"+(++i));
}
}
运行结果:
分配次数:1
分配次数:2
分配次数:3
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2464.hprof …
Heap dump file created [16991068 bytes in 0.047 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at com.ghs.test.OOMTest.main(OOMTest.java:16)
附:dump文件会在项目的根目录下生成
从上面的例子我们可以看出,在进行第4次内存分配时,发生了内存溢出。
二、栈溢出 栈空间不足时,需要分下面两种情况处理:
线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error
虚拟机在扩展栈深度时无法申请到足够的内存空间,将抛出OutOfMemberError
附:当前大部分的虚拟机栈都是可动态扩展的。
1、栈空间不足——*Error实例
public class StackSOFTest {
int depth = 0;
public void sofMethod(){
depth ++ ;
sofMethod();
}
public static void main(String[] args) {
StackSOFTest test = null;
try {
test = new StackSOFTest();
test.sofMethod();
} finally {
System.out.println("递归次数:"+test.depth);
}
}
}
执行结果:
递归次数:982
Exception in thread “main” java.lang.*Error
at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:8)
at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)
at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)
……后续堆栈信息省略
我们可以看到,sofMethod()方法递归调用了982次后,出现了*Error。
2、栈空间不足——OutOfMemberError实例
单线程情况下,不论是栈帧太大还是虚拟机栈容量太小,都会抛出*Error,导致单线程情境下模拟栈内存溢出不是很容易,不过通过不断的建立线程倒是可以产生内存溢出异常。
public class StackOOMTest {
public static void main(String[] args) {
StackOOMTest test = new StackOOMTest();
test.oomMethod();
}
public void oomMethod(){
while(true){
new Thread(new Runnable() {
@Override
public void run() {
loopMethod();
}
}).start();;
}
}
private void loopMethod(){
while(true){
}
}
}
运行结果:
……操作系统直接挂掉了
如果哪位大神能够成功模拟,还望指点一二。
三、永久代溢出
永久代溢出可以分为两种情况,第一种是常量池溢出,第二种是方法区溢出。
1、永久代溢出——常量池溢出
要模拟常量池溢出,可以使用String对象的intern()方法。如果常量池包含一个此String对象的字符串,就返回代表这个字符串的String对象,否则将String对象包含的字符串添加到常量池中。
public class ConstantPoolOOMTest {
/**
* VM Args:-XX:PermSize=10m -XX:MaxPermSize=10m
* @param args
*/
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i=1;
try {
while(true){
list.add(UUID.randomUUID().toString().intern());
i++;
}
} finally {
System.out.println("运行次数:"+i);
}
}
}
运行结果:
……比较尴尬的是,通过intern,始终无法模拟出常量池溢出,我的猜想是JDK7对常量池做了优化。
如果哪位大神成功模拟出来了,还望指点一二。
找了好久,终于弄清楚了使用string.intern()方法无法模拟常量池溢出的原因。
因为在JDK1.7中,当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用。
简单来说,就是对象实际存储在堆上面,所以,让上面的代码一直执行下去,最终会产生堆内存溢出。
下面我将堆内存设置为:-Xms5m -Xmx5m,执行上面的代码,运行结果如下:
运行次数:58162
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.lang.Long.toUnsignedString(Unknown Source)
at java.lang.Long.toHexString(Unknown Source)
at java.util.UUID.digits(Unknown Source)
at java.util.UUID.toString(Unknown Source)
at com.ghs.test.ConstantPoolOOMTest.main(ConstantPoolOOMTest.java:18)
2、永久代溢出——方法区溢出 方法区存放Class的相关信息,下面借助CGLib直接操作字节码,生成大量的动态类。
public class MethodAreaOOMTest {
public static void main(String[] args) {
int i=0;
try {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
i++;
}
} finally{
System.out.println("运行次数:"+i);
}
}
static class OOMObject{
}
}
运行结果:
运行次数:56
Exception in thread “main”
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread “main”
虽然出现了异常,但是打印的堆栈信息似乎并不是我们想要的……
四、直接内存溢出
DirectMemory可以通过-XX:MaxDirectMemorySize指定,如果不指定,默认与Java堆的最大值(-Xmx指定)一样。
NIO会使用到直接内存,你可以通过NIO来模拟,在下面的例子中,跳过NIO,直接使用UnSafe来分配直接内存。
public class DirectMemoryOOMTest {
/**
* VM Args:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m
* @param args
*/
public static void main(String[] args) {
int i=0;
try {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while(true){
unsafe.allocateMemory(1024*1024);
i++;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("分配次数:"+i);
}
}
}
运行结果:
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.ghs.test.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:20)
分配次数:27953
无法创建本地线程
最后我们在来看看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来达到创建更多线程的目的。
其中,-Xmx用来设置你的应用程序(不是JVM)能够使用的最大内存数,如果你的程序要花很大内存的话,那就需要修改缺省的设置,比如配置tomcat的时候,如果流量啊程序啊都很大的话就需要加大这个值了,BUT不要大得超过你的机器的内存。
另一个-Xms用来设置程序初始化的时候内存栈的大小,增加这个值的话你的程序的启动性能会得到提高。不过同样有前面的限制,以及受到-Xmx的限制。
总结:
- 栈内存溢出: 程序所要求的栈深度过大。
- 堆内存溢出: 分清内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用,不足则通过调大-Xms,-Xmx参数。
- 永久代溢出: Class对象未被释放,Class对象占用信息过多,有过多的Class对象。
- 直接内存溢出: 系统哪些地方会使用直接内存。