内存溢出异常 OOM
我们知道:
- JVM的内存模型
- 对象的创建和布局
开始面对最终Boss: OOM
我们的目标:
- 使用代码验证Java内存模型
- 在实际发生OOM时,通过异常信息,瞬间判断:
- 那个区域OOM
- 定位代码
- 异常处理
堆OOM
什么情况下会发生堆OOM
- 不断的在堆中创建对象
- 垃圾回收机制无法回收对象
不断创建对象通过循环就可以了,但什么情况下垃圾回收机制无法回收对象呢
- GC通过GC Roots到对象之间的可达路径来回收对象。
可作为GC Roots的对象有:
- 虚拟机栈引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 这里使用第一种方式:虚拟机栈引用,即变量,存放循环创建的对象。
具体实现:使用List集合,循环添加测试对象。
集合中大量数据很常见呀,也没见到堆OOM
是的,所以需要设置下虚拟机的内存大小,和不可扩展。
JVM 参数:
-Xmx20m : 表示设置虚拟机最大内存20m
-Xms20m : 表示设置虚拟机最小内存20m, 最大内存=最小内存,表示虚拟机不可扩展。
我用的是STS, 这个在虚拟机参数在哪设置
- Run Configuration/Debug Configuration 中有VM参数这一项
- 设置Java -> Installed JREs选中使用的jdk/jre -> edit按钮 -> 输入VM参数
那报错OOM如何分析呢
一般日志只记录报错堆栈,无法确定某个类占用百分比或GC可达性分析等等。
分析OOM, 需要堆转储快照文件。即发生OOM之前的快照将堆栈中信息以文件信息保存下来
堆转储文件怎么设置?
设置JVM参数即可:-XX:+HeapDumpOnOutOfMemoryError
表示创建堆快照文件,在OOM异常发生时。
上代码
代码:
public class HeapOOM {
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
public class OOMObject {
}
报错异常:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7740.hprof ...
Heap dump file created [27970781 bytes in 0.088 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at jvm.com.oom.heap.HeapOOM.main(HeapOOM.java:12)
使用Memory Analysis 工具分析
可以看到主线程占用15.5M(97%)的空间
而class OOMObject一共有3,241,320个没有释放,占用了97%空间
所以问题就是这个对象无法释放,导致 OOM: Java heap space
虚拟机栈OOM
什么情况下会发生虚拟机栈OOM
虚拟机栈会有两种情况:
- 栈空间不可扩展,当前虚拟机栈深度 > 虚拟机规定的栈深度, 会抛出栈溢出错误
- 栈空间可扩展,扩展时无法申请到足够内存,会抛出内存溢出异常
测试1:
- 本地测试设置栈最大内存参数:-XSs10m
- 单线程使用死递归测试,并打印当前栈深度。
- 测试结果:抛出的总是
栈溢出
异常,且栈深度在一定范围内变化 - 测试结论:可以看出,这是属于第一种情况,当前虚拟机栈深度 > 虚拟机规定的栈深度
新问题:
但栈深度在一定范围内变化,是否表示每次虚拟机规定的栈深度不同?
测试2:
修改栈最大的内存参数,数值缩小一半:-XSs5m
- 测试结果:还是抛出
栈溢出
异常,且栈深度在原来一半值左右变化 - 测试结论:也就是说,虚拟机栈深度并非虚拟机规定死的,而是通过虚拟机启动时当前最大栈空间计算出来的。
新问题:
既然是通过最大栈空间计算的,如果扩大每个栈帧大小,栈空间在扩展时,可能无法申请到足够内存而抛出内存溢出异常
测试3:
在递归方法添加多个局部变量,扩大栈帧。
测试结果:还是抛出
栈溢出
异常,局部变量越多,栈深度越小。测试结论:虚拟机栈深度的计算,是在编译期就计算好的。
新问题:
编译时怎么计算栈深度呢
我们知道:栈帧中的局部变量表在编译时就知道大小,运行时可以直接分配内存
所以编译期就知道栈帧大小,通过最大栈帧,和栈空间最大值,可以知道栈深度最大多少。
新问题:
如何模拟栈空间内存溢出?
这个栈深度是单线程情况下计算出来的,如果多线程情况下,线程越多,占用的栈空间就越多,越可能发生栈空间内存溢出异常。
但是测试案例无法模拟,因为创建很多进程在window环境下直接导致操作系统假死,Java的线程是映射到操作系统的内核线程上。
理论上: 多线程中为每个线程分配越大的内存空间,越容易出现内存溢出
原因:
- 操作系统分配给每个进程的内存是有限制的,如32位windows是2G
- 虚拟机会设置Java堆内存和方法区内存最大值,即还剩下:2G - 最大堆内存 - 最大方法区内存
- 剩下内存由虚拟机栈和本地方法栈瓜分,每个线程分配到的栈容量越大,可建立的线程数量越少。
建立新的线程时,就容易发生内存溢出异常。
以上结论待测试验证!
方法区内存溢出异常
方法区什么时候出现内存溢出异常
方法区在不同jdk版本中实现不同
- jdk1.7之前,使用永生代实现
- jdk1.8之后,使用元空间实现
由于我现在使用的是jdk1.8, 无法模拟出永生代的内存溢出,但原理基本一致。
测试步骤:
设置虚拟机参数,方法区空间最大值,且无法扩展
永生代虚拟机参数:-XX:PermSize=10M -XX:MaxPermSize=10M
元空间虚拟机参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M循环创建大量不同的类,直到内存溢出。
使用CGlib字节码动态代理方式,可以在运行时动态创建不同的类。
CGlib字节码动态代理在框架中经常遇到,如Spring框架的AOP就是使用CGlib字节码动态代理实现的。
测试代码:
public class PermOOM {
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);
}
});
}
}
static class OOMObject {
}
}
测试结果:
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
测试结论:
可以看到,当元空间内存不够时,大量的类就会造成元空间的内存溢出
所以在Spring等框架运用大量的CGlib字节码动态代理技术时,需要保证有大容量的元空间。
关于永久代有个字符串常量池的问题
String 有个intern()方法。
在jdk1.6中,会把首次遇到的字符串实例复制到永久代中,返回的也是这个永久代中这个字符串的实例.
在jdk1.7之后,不会再复制字符串实例,只是在字符串常量池中记录首次出现的实例引用。
所以会有下面代码中情况:
public class ContantsOOM {
public static void main(String[] args) {
// 指向字符串常量池中字符串
String str1 = "xuweizhen";
// str1在字符串常量池中已存在,str1.intern返回字符串常量值中首次出现的实例引用,一致
System.out.println("1 :" + (str1.intern() == str1));
// 指向堆中字符串对象
String str2 = new StringBuilder("xuwei").append("zhen").toString();
// str2.intern()在字符串常量池中已存在,不是首次出现,所以返回的是str1的字符串常量池常量,与str2不一致。
System.out.println("2 :" + (str2.intern() == str2));
// 指向堆中字符串对象
String str22 = new StringBuilder("aaa").append("bbb").toString();
/**
* str22指向堆中字符串对象引用
* str22.intern方法判断str22在字符串常量池中是否存在,str22不在字符串常量池中
* 将str22放入字符串常量池中,并返回该字符串常量池引用,所以一致。
*/
System.out.println("3 :" + (str22.intern() == str22));
// 指向堆中字符串对象
String str222 = new StringBuilder("a").append("aabbb").toString();
System.out.println("4 :" + (str222.intern() == str222));
// 指向堆中字符串对象
String str3 = new String("cccddd");
// str的new String()方法返回的是一个字符串副本,和原字符串引用并不一致
System.out.println(str3.intern() == str3);
}
}
直接内存的内存溢出异常
直接内存在什么情况下出现内存溢出异常
直接内存容量通过参数:-XX:MaxDirectMemorySize指定
可以通过Unsafe的allocateMemory方法分配直接内存,但Unsafe类只有引导类加载器才会返回实例。这里无法实现。
直接内存测试待补充!!!
想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529