引言:本文是阅读经典的《深入理解Java虚拟机》后,对第二章的内存溢出异常情况进行总结,通过实际代码实现来验证知识点的正确性。
Java的堆用于存储对象实例,只要不断地创建对象,并且保证GC root 到对象之间有可达路径,就无法被收集器回收。
在java工程的目录下新建一个类,这里命名为MemoryController,代码如下:
@RestController
public class MemoryController {
private List<TestEntity> heapList = new ArrayList<>();
/**
* -Xmx32M -Xms32M
* */
@GetMapping("/heap")
public String heap() {
long i=1l;
while(true) {
heapList.add(new TestEntity(i++, UUID.randomUUID().toString()));
}
}
}
运行大约十分钟之后,结果显示是堆内存溢出:
《深入理解Java虚拟机》书中给出这种情况的解决思路:
(1)如果是内存泄露,查GCRoot引用链,打印出引用链信息,就基本可以定位泄露代码的为位置。
(2)如果不存在泄露,也就是内存中的对象确实都必须存活,就检查虚拟机的堆参数。看看-Xmx和-Xms在机器物理内存的基础上可否再调大。
具体方法可见另一篇博客:Java虚拟机(JVM)调优和Debug的常用参数详解
不过大多数情况都是在代码层面上检查是否有对象生命周期过长或关联过多,尝试减少程序运行期的内存消耗即可。
《深入理解Java虚拟机》中称作方法区,JDK1.7进行了“去永久代”之后,就成了元数据区,用于存Class对象,一个类要被垃圾收集器回收的条件很苛刻,尤其是在框架越来越多的情况下。因此这里创建大量不回收的类对象。
首先,在工程的pom.xml中引入asm的jar包帮助实现动态生成类:
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
创建动态生成类Metaspace,代码如下:
public class Metaspace extends ClassLoader {
public static Collection<? extends Class<?>> createClasses() {
// 类持有
List<Class<?>> classes = new ArrayList<Class<?>>();
// 循环1000w次生成1000w个不同的类。
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
// 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
// 定义构造函数方法
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "" ,
"()V", null, null);
// 第一个指令为加载this
mw.visitVarInsn(Opcodes.ALOAD, 0);
// 第二个指令为调用父类Object的构造函数
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"" , "()V");
// 第三条指令为return
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
// 定义类
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
在之前的MemoryController里加metaspace()方法
@RestController
public class MemoryController {
List<Class<?>> classlist = new ArrayList<>();
/**
* -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
*/
@GetMapping("/metaspace")
public String metaspace() {
long i=1l;
while(true) { //动态生成class
classlist.addAll(Metaspace.createClasses());
}
}
}
为了能够马上看到结果,这里手动把JVM的元数据去参数修改一下,IDEA里的修改如下图
很快直接元数据区溢出,并且spring直接定位到了导致溢出的代码行
本文主要测试了两种典型的内存溢出,还有栈溢出(递归调用可能溢出)以及本机溢出以可测试,但是出现情况较少,就没有做实际的测试了。