堆内存溢出
/**
* 执行前需要配置的参数
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -Xms:最小堆内存;-Xmx:最大堆内存;
* -XX:+HeapDumpOnOutOfMemoryError:内存溢出时拉取内存堆转储快照
*/
public class HeapOOM {
/** 定义一个内部类 */
static class OOMObject{
}
public static void main(String[] args) {
//创建一个集合存放对象,防止对象被垃圾回收
ArrayList<OOMObject> list = new ArrayList<>();
//循环创建对象并放入集合中
while (true){
list.add(new OOMObject());
}
}
}
报错信息如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2990.hprof ...
Heap dump file created [27926099 bytes in 0.076 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:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at HeapOOM.main(HeapOOM.java:15)
可根据快照文件分析内存溢出的具体对象位置,根据业务中对象是否必需,我们把OOM分为两种情况:内存泄漏和内存溢出
注:具体排查方式会在后续给出。
内存泄漏:
- 概念: 指的是堆内存中大部分对象已经无关紧要了,垃圾回收器完全可以回收它们,而它们却因为被判定为不可回收条件从而导致了对象的堆积。
- 解决方案: 通过分析快照找到具体是哪个对象导致了OOM,在业务中找到该对象的使用场景,在该对象被利用完之后应当置为NULL以便垃圾收集器回收。
内存溢出:
- 概念: 堆内存中的每一个对象都依然存在利用价值,不能被垃圾回收。
- 解决方案: 适当扩大堆内存的大小,或者在代码层面关注是否可通过某些创建型设计模式进行优化。
栈内存溢出
/**
* 栈帧过多导致的SOF
* VM Args: -Xss228k
* -Xss:栈内存容量
*/
public class JavaVMStackSOF2Many {
private int stackLength = 1;
/**
* 递归,无限增加栈帧
*/
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF2Many oom = new JavaVMStackSOF2Many();
try {
oom.stackLeak();
} catch (Exception e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
报错信息如下:
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:14)
at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
······
/**
* 栈帧过大导致的SOF
* VM Args: -Xss228k
* -Xss:栈内存容量
*/
public class JavaVMStackSOF2Big {
private static int stackLength = 1;
/**
* 扩大栈帧容量
*/
public static void stackLeak(){
long unused1,unused2,unused3,unused4,unused5,unused6,unused7,unused8,unused9,unused10,
unused11,unused12,unused13,unused14,unused15,unused16,unused17,unused18,unused19,unused20,
unused21,unused22,unused23,unused24,unused25,unused26,unused27,unused28,unused29,unused30,
unused31,unused32,unused33,unused34,unused35,unused36,unused37,unused38,unused39,unused40,
unused41,unused42,unused43,unused44,unused45,unused46,unused47,unused48,unused49,unused50,
unused51,unused52,unused53,unused54,unused55,unused56,unused57,unused58,unused59,unused60,
unused61,unused62,unused63,unused64,unused65,unused66,unused67,unused68,unused69,unused70,
unused71,unused72,unused73,unused74,unused75,unused76,unused77,unused78,unused79,unused80,
unused81,unused82,unused83,unused84,unused85,unused86,unused87,unused88,unused89,unused90,
unused91,unused92,unused93,unused94,unused95,unused96,unused97,unused98,unused99,unused100;
stackLength++;
stackLeak();
unused1=unused2=unused3=unused4=unused5=unused6=unused7=unused8=unused9=unused10=
unused11=unused12=unused13=unused14=unused15=unused16=unused17=unused18=unused19=unused20=
unused21=unused22=unused23=unused24=unused25=unused26=unused27=unused28=unused29=unused30=
unused31=unused32=unused33=unused34=unused35=unused36=unused37=unused38=unused39=unused40=
unused41=unused42=unused43=unused44=unused45=unused46=unused47=unused48=unused49=unused50=
unused51=unused52=unused53=unused54=unused55=unused56=unused57=unused58=unused59=unused60=
unused61=unused62=unused63=unused64=unused65=unused66=unused67=unused68=unused69=unused70=
unused71=unused72=unused73=unused74=unused75=unused76=unused77=unused78=unused79=unused80=
unused81=unused82=unused83=unused84=unused85=unused86=unused87=unused88=unused89=unused90=
unused91=unused92=unused93=unused94=unused95=unused96=unused97=unused98=unused99=unused100 = 0L;
}
public static void main(String[] args) {
try {
stackLeak();
} catch (Exception e) {
System.out.println("stack length: " + stackLength);
throw e;
}
}
}
报错信息如下:
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:25)
at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
···
由此可知,不论是栈帧过多,还是栈帧过大,都会导致SOF异常。
注:
- 由于HotSpot虚拟机不支持栈容量动态扩展,所以没有OOM异常,而Classic虚拟机支持栈内存的动态扩展,当栈内存扩展时无法申请到足够的内存时,也会报OOM异常。
- 如果方法中执行的是创建线程并使用线程的操作,则也会抛OOM异常。其主要原因是:每创建一个线程,就会对应分配一个虚拟机栈(线程栈),操作系统分配给每个进程的内存是有限制的,我们可以粗略的认为进程内存 = 虚拟机栈+本地方法栈内存+堆内存+方法区内存+其他,在其他内存(包括堆内存、方法区内存等)都已分配完成的情况下,剩余部分都由虚拟机栈和本地方法栈来分配了,在总内存不够充裕的情况下,系统无法给新线程开辟栈空间,就会导致OOM,其本质还是总的空间内存不足导致的,而非栈内存不足。具体代码如下(善意提示:请勿轻易尝试,如果非要手贱的话,请保存当前工作任务):
/**
* VM Args:-Xss2M
*/
public class JavaVMStackOOM {
/**
* 给线程定义一个死循环方法,保持线程活跃
*/
private void dontStop() {
while (true) {
}
}
/**
* 创建线程
*/
public void stackLeakByThread() {
while (true) {
new Thread(() -> {
dontStop();
}).start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
这里直接复制《深入理解Java虚拟机》中的错误信息:
Exception in Thread "main" java.lang.OutOfMemoryError: unable to create native thread
方法区和运行时常量池溢出
由于本人使用的M1芯片的Mac系统,无法使用旧版本的JDK,所以异常摘自《深入理解Java虚拟机》
/**
* VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M
* 注:设置的是永久代内存,只适用于JDK1.6及之前版本
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//创建集合存储字符串保证字符串对象不被垃圾回收
HashSet<String> set = new HashSet<>();
short i = 0;
while (true){
//将字符串放入方法区
set.add(String.valueOf(i++).intern());
}
}
}
报错信息如下:
java.lang.OutOfMemoryError: PermGen space
而在JDK1.8中,永久代已完全退出历史舞台,取而代之的是元空间,代码无需更改,只需更改虚拟机参数即可:
/**
* VM Args: -Xmx6M
* 注:设置的是永久代内存,只适用于JDK1.6及之前版本
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//创建集合存储字符串保证字符串对象不被垃圾回收
HashSet<String> set = new HashSet<>();
short i = 0;
while (true){
//将字符串放入方法区
set.add(String.valueOf(i++).intern());
}
}
}
报错信息如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:706)
at java.util.HashMap.putVal(HashMap.java:665)
at java.util.HashMap.put(HashMap.java:614)
at java.util.HashSet.add(HashSet.java:220)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:15)
可以看到,将最大堆内存设置为6M时,在HashMap扩容时就发生了OOM(HashSet底层是HashMap),几乎看不到方法区内存溢出,原因是在JDK1.8时,方法区的实现主要通过本地内存开辟的元空间来实现。
关于元空间的配置参数介绍:
- -XX:MaxMetaspaceSize:设置元空间最大值,默认为-1(不限制,但受限于本地内存)
- -XX:MetaspaceSize:元空间初始大小,达到该值触发垃圾回收。垃圾回收器会根据垃圾回收情况动态调整该值:如果垃圾回收时释放了大量空间,则会适当降低该值;如果释放的空间很微小,会适当提高该值(但不会超过设置的最大值)
- -XX:MinMetaspaceFreeRatio:在垃圾回收之后,控制最小的元空间剩余容量百分比,以减少因为元空间不足导致的垃圾回收频率。
- -XX:MinMetaspaceFreeRatio:控制最大的元空间剩余容量百分比。
注:类爆炸会引起方法区内存溢出。
本地直接内存溢出
直接内存可通过参数 -XX:MaxDirectMemorySize 来设置,如果不指定,会和最大堆内存 -Xmx 保持一致
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
//反射获取Unsafe实例对象
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
while (true){
//申请分配内存 1MB
System.out.println("分配内存1MB...");
unsafe.allocateMemory(_1MB);
}
}
}
理论上应当会报OOM,但我在自己电脑上并没有报异常,具体问题有待分析…
注:DirectMemory导致的OOM并不会在Heap Dump中凸显出来,如果程序中出现了OOM而又没有较大或较多的异常对象时,应当考虑是否使用了NIO等利用了直接内存的相关技术。