在JVM虚拟机规范中,Java虚拟机运行时数据区域除了程序计数器(Program Counter Register)外都有可能出现OutOfMemoryError
的情况,使用Hotspot虚拟机简单的模拟堆栈内存溢出的场景,方便快速定位是什么区域的内存溢出。
堆
通过VM参数设置Java堆的大小,避免堆可扩展内存(设定-Xms和Xmx一样可避免堆自动扩展);
通过设定-XX:+HeapDumpOnOutOf-MemoryError
可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照。
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author Vicente
* @version 1.0
* @date 2020/4/5 10:28
*/
public class TestHeapOOM {
public static void main(String[] args) {
List<TestHeapOOM> list = new ArrayList<TestHeapOOM>();
while (true) {
list.add(new TestHeapOOM());
}
}
}
设置启动参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3676.hprof ...
Heap dump file created [28279988 bytes in 0.099 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:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.oom.TestHeapOOM.main(TestHeapOOM.java:19)
使用IDEA和Eclipse都可以设置启动时的参数。
堆转储快照文件一般生成后位于你的work space,拿到文件后要对快照文件进行分析,可以采用不同的工具来帮助我们分析,这里推荐两种:
- jhat
在IDEA或者Eclispe的终端控制台直接输入命令
jhat java_pid13232.hprof
当dump的文件过大时需要设置jhat参数:jhat -J-Xmx2048m java_pid13232.hprof,默认-Xmx为1024
对堆快照进行分析
Reading from java_pid13232.hprof...
Dump file created Sun Apr 05 10:54:06 CST 2020
Snapshot read, resolving...
Resolving 818818 objects...
Chasing references, expect 163
dots................................................................................................................
Eliminating duplicate
references..........................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
通过访问http://localhost:7000即可查看分析结果,对象内存分配的大小等信息。
- mat
工具下载地址:https://eclipse.org/mat/downloads.php,选择需要下载的版本,windows版本下载后是一个压缩包,直接解压运行即可。
打开需要分析的堆转储文件,分析后会展示一个概要预览
Leak Suspects » Leaks » Problem Suspect 在这里面可以看到对象的个数,对象占用大小等信息,这里包含两个重要信息
Generally speaking, shallow heap of an object is its size in the heap and retained size of the same object is the amount of heap memory that will be freed when the object is garbage collected.
具体解释可以参考:https://help.eclipse.org/2020-03/index.jsp
- Shallow Heap:某个对象自身大小,不包含其引用对象的大小;
- Retained Heap:某个对象在发生GC回收时,如果被释放,其释放内存的大小,这就要包含其引用的对象占用堆内存的大小。
这里具体使用就不再描述,可以参考官方文档。
栈
Java运行时数据区包含虚拟机栈和本地方法栈,在Hotspot虚拟机实现中对于本地方法栈的参数(-Xoss)设定并无实际效果,只通过-Xss参数来模拟栈的内存溢出。《Java虚拟机规范》中指出:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowError异常
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出OutOfMemoryError异常
设置运行参数VM Args:-Xss128k
/**
* VM Args:-Xss128k
* @author Vicente
* @version 1.0
* @date 2020/4/5 12:39
*/
public class TestStackOverflow {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
TestStackOverflow overflow = new TestStackOverflow();
try {
overflow.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + overflow.stackLength);
throw e;
}
}
}
运行结果:
Exception in thread "main" java.lang.StackOverflowError
at com.oom.TestStackOverflow.stackLeak(TestStackOverflow.java:17)
at com.oom.TestStackOverflow.stackLeak(TestStackOverflow.java:17)
//省略...
stack length:36984
//省略...
根据操作系统的不同和Java虚拟机版本的不同,栈容量的最小值也会有所不同,改变栈容量的大小或者栈帧过大时都会导致StackOverflowError异常。
Hotspot虚拟机不支持扩展栈内存,除非在创建线程申请内存就不足会导致OutOfMemoryError异常,其他情况都是在运行时因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
不同的虚拟机实现有着不同的细节处理,其他虚拟机实现如果是可扩容栈空间,栈容量不足时会抛出OutOfMemoryError异常,当遇到OutOfMemoryError时要先判断是栈空间还是堆内存的异常。
方法区
在JDK6之前的Hotspot虚拟机中方法区被设置在永久代中,运行时常量池也属于方法区的一部分,可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,在JDK7开始逐步的去永久代,到了JDK8就开始使用元空间(meta-space)来实现方法区,保存程序运行时的数据。
使用JDK6,设定永久代参数-XX:PermSize=2M -XX:MaxPermSize=2M
public class TestConstantPoolOOM {
public static void main(String[] args) throws Throwable {
//使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
// 在short范围内足以让6MB的PermSize产生OOM了
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
//String.valueOf(i++).intern();
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.oom.TestConstantPoolOOM.main(TestConstantPoolOOM.java from InputFileObject:21)
可以看到OutOfMemoryError后面指明了内存溢出的位置PermGen space
;在JDK8中使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制,也不会出现异常,因为从JDK7开始常量池已经从永久代移到了Java堆的位置,此时限制Java堆的大小便会抛出异常,定位异常的位置。
设置上面代码的运行参数:-Xms6m -Xmx6m
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:704)
at java.util.HashMap.putVal(HashMap.java:663)
at java.util.HashMap.put(HashMap.java:612)
at java.util.HashSet.add(HashSet.java:220)
at com.oom.TestConstantPoolOOM.main(TestConstantPoolOOM.java:20)
可以看到,程序运行抛出java.lang.OutOfMemoryError异常。
方法区的异常也是一种常见异常,一个类被垃圾回收期回收的条件是比较苛刻的,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况,比如传统项目中大量的jsp文件,jsp会被编译成Java类,容易抛出方法区异常,在JDK8以后永久代不再存在,元空间的出现使得,正常创建对象的过程很难出现方法区的内存溢出,不过Hotspot也提供了一些参数设置保护元空间。
- XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
- XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集,收集器会对该值进行调整:如果释放大量的空间,适当降低该值;如果释放了很少的空间,在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
- XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。
- XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
直接内存
直接内存可以理解为堆外内存,通过参数-XX:MaxDirectMemorySize来设定,如果不设置默认与Java堆内存的最大值相同。NIO会使用直接内存,使用Unsafe类来模拟直接内存溢出的情况。
public class TestDirectMemoryOOM {
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);
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.oom.TestDirectMemoryOOM.main(TestDirectMemoryOOM.java:24)
当直接内存溢出时,Dump文件并没有太多错误信息,要考虑是否间接使用NIO了。
总结
- 当Java虚拟机抛出OutOfMemoryError错误时,判断是Java内存哪一块区域抛出的错误,定位堆,栈
- 使用工具分析堆转储快照,判断是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 当发生内存泄漏可以通过工具查看GC Roots引用链,分析为什么垃圾回收器无法回收,判定对象创建的位置,是否有可回收对象
- 如果是内存溢出,根据硬件性能是否可以(使用-Xms和-Xmx)扩展堆内存大小,或者从代码角度去优化
参考资料:
《深入理解Java虚拟机 第三版》https://help.eclipse.org/2020-03/index.jsp