这一部分属于实践啦,将会模拟各种 OOM 和 SOF 的情况,以此帮助大家在工作中遇到相关错误时,能够知道如何去处理。
基础知识
由上一 part 的内容我们知道,在 Java 的运行时数据区中,除了程序计数器 PC,其他区域都定义了相关的内存泄漏 / 内存溢出错误,主要包括 OutOfMemoryError 和 StackOverFlowError 两种,具体可以查看上一篇文章。
本文将对各个数据区的内存泄漏 / 内存溢出进行模拟,因此必不可少的需要对 JVM 的参数进行修改。以 IDEA 为例,具体的修改方式如下:


然后点击 Apply 确认就好啦!(注意不要把 VM 参数写到 Program arguments 里噢!)
Java 堆溢出
Java 堆用于存储对象实例,因此要模拟堆溢出,只需要用一个死循环不断生成新的对象即可。在实验之前,我们通过加入参数 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
将堆的大小限制在 20MB 且不可扩展,另外在内存溢出时生成 Dump 文件以便分析。
java
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) throws Exception {
// VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
List<OOMObject> list = new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
运行后可以得到下面这样的结果:
说明发生了 OOM 异常,且 OOM 区域是 Java 堆区。此时我们可以对 Dump 文件进行分析,分辨出是内存泄漏还是内存溢出。如果是内存泄漏,则可进一步对泄漏对象的 GC Roots 引用链进行分析,从而定位到泄漏代码的具体位置;如果是内存溢出,说明内存中的对象都是必须存活的,那就可以尝试修改 JVM 参数( -Xms
和 -Xmx
) ,并检查代码是否存在设计不合理的地方,如某些对象的生命周期过长等。具体的处理过程将在第三章结合垃圾回收进行分析。
虚拟机栈和本地方法栈溢出
对于栈区, JVM 规范中定义了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常;
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。
由于 HotSpot 虚拟机不支持栈的动态扩展,接下来的实验将只会模拟第一种异常。
要模拟第一种异常有两种方式:一是不定义额外变量,仅通过无限递归申请无穷的栈帧,这种情况下每个栈帧占用空间少,异常时的堆栈深度大;二是通过定义大量本地变量,增大每个栈帧的长度,这种情况下每个栈帧占用空间大,异常时的堆栈深度小。下面通过加入参数 -Xss128k
限制栈的大小来模拟两种情况下的 SOF 异常。
- 情况一:
java
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
// -Xss128k
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行后可以得到下面这样的结果:
- 情况二:
java
public class JavaVMStackSOF {
private static int stackLength = 0;
public static void test() {
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++;
test();
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 = 0;
}
public static void main(String[] args){
// -Xss128k
try {
test();
} catch (Throwable e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
运行后可以得到下面这样的结果:
如果栈的大小允许动态扩展,那上面两个示例均将产生 OOM 异常,而非 SOF。
另外在多线程下,HotSpot 也会产生 OOM 异常。因为操作系统为 JVM 进程分配的内存是有限的,去掉堆区、方法区和其他一些资源后,剩下的就由不同线程的栈区瓜分。因此当线程数量太多,导致操作系统分配给虚拟机的内存被使用完时,会产生 OOM 异常。由上述描述可以得知,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
加入参数 -Xss2M
模拟上述情况。
java
public class JavaVMStackOOM {
private void dontStop() {
while(true) {}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
需在 32 位操作系统下模拟,结果将是
OutOfMemoryError: unable to create native thread
,本文不做展示。
对于这种多线程导致的内存溢出,在不能减少线程数量或者更换 64 位虚拟机的情况下,只能通过减少最大堆和减少栈容量来获取更多的线程。
方法区和运行时常量池溢出
在 JDK 8 之后,字符串常量池被保留在了堆中,而永久代则被元空间取代,退出了历史舞台,因此方法区和运行时常量池的溢出变得不再那么常见。
这里简单介绍一下控制元空间的几个参数:
-XX:MaxMetaspaceSize
:设置元空间最大值,默认为 -1,即仅受限于本地内存大小。-XX:MetaspaceSize
:指定元空间的初始大小,到达该值就会触发垃圾回收进行类卸载,同时收集器会对该值进行调整:如果释放了大量空间则适当降低该值;如果释放了很少的空间,则在不超过MaxMetaspaceSize
的情况下适当提高该值。-XX:MinMetaspaceFreeRatio
:在垃圾回收之后控制最小的元空间剩余容量的百分比,即空闲比小于该值时会适当扩容,可减少因为元空间不足导致的垃圾回收频率。
本机直接内存溢出
直接内存的容量大小可通过 -XX:MaxDirectMemorySize
指定,默认与 Java 堆最大值一致。
下面通过加入参数 -XX:MaxDirectMemorySize=10M
来模拟直接内存溢出。模拟原理是通过 Unsafe
类的 allocateMemory()
方法来分配直接内存。Unsafe
类的对象需要通过反射获取。
java
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Throwable {
// -XX:MaxDirectMemorySize=10M
// 通过反射获取Unsafe类的第一个成员变量,即"Unsafe theUnsafe"
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
// 取消安全检查
unsafeField.setAccessible(true);
// 通过get得到一个Unsafe对象,geet(null)是因为这是一个stati成员变量
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true) {
unsafe.allocateMemory(_1MB);
}
}
}
运行后可以得到下面这样的结果:
由直接内存导致的内存溢出,一个很明显的特征是在 Dump 文件中不会看到上面明显的异常情况。因此当 Dump 文件很小,程序中又使用了 DirectMemory 时,可以重点检查这方面。