我将通过代码实例验证java内存各个区域(除了程序计数器)出现OutOfMemoryError异常的场景。
1.Java堆溢出
/**堆溢出:通过不断创建无法被回收的对象实例
* VM Args:-Xms16M(堆初始) -Xmx16M(堆最大) -Xmn16M(新生代) -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\java\java_log\heap_dump
* @author zsc
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
运行后出现异常:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\java\java_log\heap_dump\java_pid9548.hprof ...
Heap dump file created [19186297 bytes in 0.053 secs]
使用jvisualvm
打开dump文件,可以看到对象实例OOMObject占满了空间:
分析堆的OOM异常时,要确认是出现内存泄漏还是内存溢出
内存溢出 | 内存泄漏 | |
---|---|---|
出现场景 | 申请内存时内存不够了 | 申请的内存无法释放,编码存在问题,持有了无用对象没释放 |
解决方法 | 检查堆参数-Xmx,-Xms看是否可调大 | 查看泄漏对象到GC Roots的引用链 |
2.虚拟机栈和本地方法栈溢出
- HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以-Xoss(本地方法栈大小)参数无效,栈容量由-Xss设定。
- 如果线程请求的栈深度大于虚拟机允许的,将抛出
StackOverflowError
- 如果扩展栈时申请不到足够空间,将抛出
OutOfMemoryError
/**栈溢出测试:通过递归耗尽栈,出现StackOverflowError异常
* VM Args:-Xss128k
* @author zsc
*/
public class StackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOF sof = new StackSOF();
try {
sof.stackLeak();
} catch (Throwable e) {
System.out.print("栈深度:" + sof.stackLength);
throw e; //抛出,不能把异常吃了
}
}
}
执行结果:
栈深度:9823 actionable tasks: 2 executed, 1 up-to-date
Exception in thread "main" java.lang.StackOverflowError
at com.zsc.jvm.oom.StackSOF.stackLeak(StackSOF.java:10)
- 单线程下,栈帧太大还是虚拟机栈容量太小,都是抛出
StackOverflowError
。 - 创建多线程时,如果给每个线程分配的栈内存较大 ,容易出现
OutOfMemoryError
内存溢出异常。(如32位系统内存为2G,减去Xmx,MaxPermSize等,剩下的已不多)
/**创建多个线程(不断分配所需栈内存)导致内存溢出
* 注意 :运行前保存好工作,windows可能假死(Java线程是映射到内核线程上的)
* VM Args:-Xss16m (设大些)
* @author zsc
*/
public class StackOOM {
private void donStop() { while (true) {} }
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
donStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
StackOOM oom = new StackOOM();
oom.stackLeakByThread();
}
}
3.方法区和运行时常量池溢出
3.1 运行时常量池
运行时常量池是方法区的一部分
/**运行时常量池的内存溢出
*
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M 使用JDK1.6进行测试才会出现OOM:PermGen space
* -Xms20m -Xmx20m 减少堆大小
* -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\java\java_log\heap_dump
* @author zsc
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用list保持常量池中字符串的引用,避免被GC回收
List<String> list = new ArrayList<>();
int i = 0; //10m的PermSize在integer范围内足够产生OOM
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
- JDK1.6前常量池在永久代内,以上测试要在JDK1.6才会出现OOM:PermGen space。
String.intern()
是一个native方法:如果字符串常量池中已有该字符串,返回;否则加到常量池,返回引用。- JDK1.7字符串常量池移到堆中,运行时常量池剩下的还在方法区。
- JDK1.8用元空间(
Metaspace
)取代永久代实现方法区,字符串常量池依然在堆,运行时常量池还在方法区。 - 因此,以上代码实例运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.zsc.jvm.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:19)
GC overhead limit exceeded表示:执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。
3.2 方法区
/**通过CGLib大量动态生成Class,使方法区内存溢出
*
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M 需要JDK1.7以前,才出现PermGen space不足
* JDK1.8元空间实现的方法区只受本地内存限制
* @author zsc
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
4.直接内存溢出
-XX:MaxDirectMemorySize
指定直接内存大小,不指定时默认与Java堆最大值(-Xmx)一样。
测试用例:
/**
* VM Args:-Xms16M(堆初始) -Xmx16M(堆最大) -XX:MaxDirectMemorySize=10M
* @author zsc
*/
public class DirectMemoryOOM {
private static final int _1M = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
执行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.zsc.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
直接内存溢出时,在Heap dump文件中不会看见明显的异常。Dump较小