常见内存溢出异常
本文来说下常见内存溢出异常文章目录概述概述在JVM内存区域中,除了程序计数器外,Java虚拟机的其他运行时区域都有可能发生OutOfMemoryError的异常,下面分别给出验证:
本文来说下常见内存溢出异常
文章目录
概述
在JVM内存区域中,除了程序计数器外,Java虚拟机的其他运行时区域都有可能发生OutOfMemoryError的异常,下面分别给出验证:
Java堆溢出
Java堆能够存储对象实例。通过不断地创建对象,并保证GC Roots到对象有可达路径来避免垃圾回收机制清除这些对象。
当对象数量到达最大堆的容量限制时就会产生OutOfMemoryError异常。
设置JVM启动参数:-Xms20M设置堆的最小内存为20M,-Xmx20M设置堆的最大内存和最小内存一样,这样可以防止Java堆在内存不足时自动扩容。-XX:+HeapDumpOnOutOfMemoryError参数可以让虚拟机在出现内存溢出异常时Dump出内存堆运行时快照。
HeapOOM.java
/**
* VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
public static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
测试运行结果:
打开Java VisualVM导出Heap内存运行时的dump文件
HeapOOM对象不停地被创建,堆内存使用达到99%。垃圾回收器不断地尝试回收但都以失败告终。
分析:遇到这种情况,通常要考虑内存泄露和内存溢出两种可能性。
如果是内存泄露:
进一步使用Java VisualVM工具进行分析,查看泄露对象是通过怎样的路径与GC Roots关联而导致垃圾回收器无法回收的。
如果是内存溢出
通过Java VisualVM工具分析,不存在泄露对象,也就是说堆内存中的对象必须得存活着。就要考虑如下措施:
- 从代码上检查是否存在某些对象生命周期过长、持续状态时间过长的情况,尝试减少程序运行期的内存。
- 检查虚拟机的堆参数(-Xmx与-Xms),对比机器的物理内存看是否还可以调大。
虚拟机和本地方法栈溢出
关于虚拟机栈和本地方法栈,分析内存异常类型可能存在以下两种:
- 如果现场请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,可能会抛出OutOfMemoryError异常。
可以划分为两类问题,当栈空间无法分配时,到底时栈内存太小,还是已使用的栈内存过大。
StackOverflowError异常
- 使用-Xss参数减少栈内存的容量,异常发生时打印栈的深度。
- 定义大量的本地局部变量,以达到增大栈帧中的本地变量表的长度。
设置JVM启动参数:-Xss128k设置栈内存的大小为128k。
JavaVMStackSOF.java
/**
* VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
private void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("Stack length: " + oom.stackLength);
throw e;
}
}
}
测试结果:
分析:在单个线程下,无论是栈帧太大还是虚拟机栈容量太小,当无法分配内存的时候,虚拟机抛出的都是StackOverflowError异常
OutOfMemoryError异常
- 不停地创建线程并保持线程运行状态。
JavaVMStackOOM.java
/**
* VM Args: -Xss2M
*/
public class JavaVMStackOOM {
private void running() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
running();
}
}).start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
测试结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
上述测试代码运行时存在较大的风险,可能会导致操作系统假死,这里就不亲自测试了,引用作者的测试结果。
方法区和运行时常量池溢出
运行时常量池内存溢出测试
运行时常量和字面量都存放于运行时常量池中,常量池又是方法区的一部分,因此两个区域的测试是一样的。 这里采用String.intern()进行测试:
String.intern()是一个native方法,它的作用是:如果字符串常量池中存在一个String对象的字符串,那么直接返回常量池中的这个String对象; 否则,将此String对象包含的字符串放入常量池中,并且返回这个String对象的引用。
设置JVM启动参数:通过-XX:PermSize=10M和-XX:MaxPermSize=10M限制方法区的大小为10M,从而间接的限制其中常量池的容量。
RuntimeConstantPoolOOM.java
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池的引用,避免Full GC回收常量池
List<String> list = new ArrayList<>();
// 10MB的PermSize在Integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
测试结果分析:
JDK1.6版本运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
JDK1.6版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError异常。 而JDK1.7及以上的版本则不会得到相同的结果,它会一直循环下去。
方法区内存溢出测试
方法区存放Class相关的信息,比如类名、访问修饰符、常量池、字段描述、方法描述等。对于方法区的内存溢出的测试,基本思路是在运行时产生大量类字节码区填充方法区。
这里引入Spring框架的CGLib动态代理的字节码技术,通过循环不断生成新的代理类,达到方法区内存溢出的效果。
JavaMethodAreaOOM.java
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {
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 obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
private static class OOMObject {
public OOMObject() {
}
}
}
JDK1.6版本运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
测试结果分析:
JDK1.6版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError异常。 而JDK1.7及以上的版本则不会得到相同的结果,它会一直循环下去。
直接内存溢出
本机直接内存的容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。
测试场景
直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:
设置JVM启动参数:-Xmx20M指定Java堆的最大内存,-XX:MaxDirectMemorySize=10M指定直接内存的大小。
DirectMemoryOOM.java
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
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);
}
}
}
测试结果:
测试结果分析:
由DirectMemory导致的内存溢出,一个明显的特征是Heap Dump文件中不会看到明显的异常信息。 如果OOM发生后Dump文件很小,并且程序中直接或者间接地使用了NIO,那么就可以考虑一下这方面的问题。
参考
- 周志明,深入理解Java虚拟机:
- JVM高级特性与最佳实践,机械工业出版社
本文小结
本文详细介绍了常见的内存溢出异常。
更多推荐
所有评论(0)