上篇文章中,记录了Java内存模型中运行时数据区的划分,每个数据区域都会可能伴随着内存溢出异常。
大致分为2类:OutOfMemoryError、StackOverflowError
一、Java堆内存溢出
Java堆是用于存放Java对象,分别在新生代和老年代。可以使用。一般情况下,新创建的对象都会在新生代中,当对象生存到一定年龄后会被移动到老年代中,当然也有一些内存占用较大的对象直接分配到老年代中。
- -Xms设置最小的Java堆和-Xmx设置最大的Java堆
- 参数-XX:+HeapDumpOnOutOfMemoryError让当内存溢出时Dump出当前内存堆转储快照
import java.util.ArrayList;
import java.util.List;
/**
* Created by ZRD on 2016/09/29.
* 测试Java堆内存溢出
*/
public class HeapOutOfMemory {
static class TestObject { }
/**
* VM args: Xms20m Xmx20m -XX:+HeapDumpOnOutOfMemoryError XX:HeapDumpPath=D:\\maven2011\\workspace\\intellij_Idea\\gavin\\gavin-dev\\jvm
*/
public static void testHeap() {
System.out.println("Java Runtime >>>> total=" + Runtime.getRuntime().totalMemory()
+ "|free=" + Runtime.getRuntime().freeMemory());
List<TestObject> list = new ArrayList<TestObject>();
while (true) {
TestObject oomObject = new TestObject();
list.add(oomObject);
}
}
public static void main(String[] args) {
testHeap();
}
}
运行结果:
Java Runtime >>>> total=20316160|free=19727648
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid46876.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2760)
at java.util.Arrays.copyOf(Arrays.java:2734)
at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
at java.util.ArrayList.add(ArrayList.java:351)
at com.gavin.memory.HeapOutOfMemory.testHeap(HeapOutOfMemory.java:26)
at com.gavin.memory.HeapOutOfMemory.main(HeapOutOfMemory.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Heap dump file created [24828355 bytes in 0.246 secs]
内存溢出时抛出 java.lang.OutOfMemoryError: Java heap space
当内存发生Java heap space时,有可能是内存泄露和内存溢出。
内存泄露是GC无法回收已经不被使用的Java对象,到时内存没办法得到及时回收,因此需要查看是否存在对象不使用时GC Roots仍然有对对象保存着引用计数
而内存溢出是JVM启动的堆内存大小不足以给新的对象分配内存空间。
解决Java堆内存溢出的问题,一般在生产环境中需要打印堆转存快照,然后使用内存映像分析工具对Dump出来的堆转存快照进行分析。要确认内存中的对象是否是必要存在,也就是要确定内存泄露(Memory Leak)还是内存溢出(Memory Overflow)
如果确定为内存泄露,可以进一步通过工具查看堆中的对象到GC Roots的引用链。掌握了引用链信息,确认内存泄露的代码位置,并推导出什么原因导致垃圾回收器无法自动回收他们。
如果为内存溢出,需要检查虚拟机的参数-Xms -Xmx与物理机器对比。
另外上述的追查过程,优化结合代码去做,是否是程序编写存在问题,例如某些对象生命周期不宜多长等待
二、Java栈和Native栈
在HotSpot中,JVM参数的配置并不却分Java栈和Native栈。实际上对于HotSpot来说,虽然-Xoss(设置本地方法栈大小)是存在的,但是无效的。
Java虚拟机规范中规定了两种异常:
- 线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError
- 虚拟机扩展栈时,无法申请到足够的内存空间,则抛出OutOfMemoryError
下面方式使用单线程模拟
两种方式测试:
1. 使用-Xss减少栈的内存容量
2. 在方法类定义大量的局部变量,目的为增大栈帧中本地变量表的长度,以增大栈帧的容量
/**
* Created by ZRD on 2016/09/29.
*
* 测试虚拟机栈溢出
*/
public class StackOverflow {
/**
*
* VM args: -Xss128k
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
StackOverflow stackOverflow = new StackOverflow();
try {
stackOverflow.stackLeak();
} catch (Exception e) {
System.out.println("stack lenth : " + stackOverflow.stackLen);
throw e;
}
}
private int stackLen = 1;
public void stackLeak() {
stackLen++;
stackLeak();
}
}
实验结论
无论栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出StackOverflowError异常
上述的测试是单线程,如果不局限于单线程,通过不断地建立线程的方式会导致内存溢出异常。这种情况下,为每个线程的栈分配的内存越大,反而更容易产生内存异常
package com.gavin.jvm.memory;
/**
* Created by ZRD on 2016/09/29.
*
* 测试虚拟机栈溢出(多线程模式下)
*/
public class StackOverflowMultiThread {
/**
*
* VM args: -Xss2M (可以调大些来观察)
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
StackOverflowMultiThread stackOverflow = new StackOverflowMultiThread();
try {
stackOverflow.stackLeakByThread();
} catch (Exception e) {
throw e;
}
}
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
}
这个时候得出以下结论:
* 多线程环境下,物理机器内存一定(物理内存中除去堆容量,方法区容量,剩下的内存就是栈可以使用的内存),如果为每个线程分配的栈内存越大,虚拟机中可以创建的线程就会越少,过多时会容易产生内存溢出
运行上面2段代码时特别注意,windows系统会卡死
三、方法区和运行时常量池
- 运行时常量池是方法区的一部分
String.intern()一个navtive方法,作用是:如果字符串常量池中已经包含一个等于此对象的字符串,则会返回常量池中的字符对象;否则将此字符串加入常量池中。
也就是,intern()会把首次遇到的字符串实例放入到永久代中,并返回永久代中字符串实例
下面代码使用String.intern()模拟永久代溢出,JDK6才会出现。
/**
* Created by ZRD on 2017/02/13.
* 测试方法区(永久代)溢出
*/
public class RuntimeConstantPoolOOM {
/**
* VM Args: -XX:+PermSize=10M -XX:MaxPermSize=10M
* @param args
*/
public static void main(String[] args) {
// 使用List保持着常量池的引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
我是在JDK7中运行的,会一直运行下去。不会抛出OutOfMemoryError异常,也不会提示“PermGen space”。
四、本机直接内存溢出
直接内存容量 -XX:MaxDirectMemorySize指定,如果不指定,默认与-Xmx大小一致。
/**
* Created by ZRD on 2017/02/13.
*
* 测试直接内存溢出
*
*/
public class DirectMemoryOOM {
private static final int _1MB = 1 * 1024 * 1024;
/**
* VM Args: -Xmx20m -XX:DirectMemorySize=10M
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
由于使用直接内存导致的溢出,一个很明显的特征是在Heap Dump文件中不会明显看见的异常,如果发现OOM之后Dump文件很小,而程序中有直接或者间接使用NIO,那就检查下是不是这个方面的原因