相关文章:
这里我们通过 jmap 和 MAT 来分析下内存溢出的问题
一、堆内存溢出示例
-
User.java
@Data public class User { private int id; private String name; public User(int id, String name) { this.id = id; this.name = name; } }
-
MemoryController.java
@RestController public class MemoryController { private List<User> userList = new ArrayList<>(); /** * -Xms64M -Xmx64M */ @GetMapping(value = "/heap") public void heap() { AtomicInteger i = new AtomicInteger(0); while (true) { userList.add(new User(i.getAndIncrement(), UUID.randomUUID().toString())); } } }
-
如上所示,在 Controller 层里,我们编写了一个 heap() 方法,在其内部无限循环地往 userlist 中添加 User 对象,直至内存溢出 (为了快速达到效果,这里我们将初始堆大小 (-Xms) 和最大堆大小 (-Xmx) 都设置为 64 M)
-
运行项目后,我们通过浏览器来调用该接口,直至内存溢出
二、非堆内存溢出示例
-
pom.xml
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>7.1</version> </dependency>
-
ClassUtil.java
public class ClassUtil extends ClassLoader { public static List<Class<?>> createClasses() { List<Class<?>> classes = new ArrayList<>(); // 循环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); // 定义构造函数<init>方法 MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); // 第一个指令为加载this mw.visitVarInsn(Opcodes.ALOAD, 0); // 第二个指令为调用父类Object的构造函数 mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); // 第三条指令为return mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); ClassUtil test = new ClassUtil(); byte[] code = cw.toByteArray(); // 定义类 Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } return classes; } }
-
MemoryController.java
@RestController public class MemoryController { private List<Class<?>> classList = new ArrayList<>(); /** * -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M */ @GetMapping(value = "/nonheap") public void nonheap() { while (true) { classList.addAll(ClassUtil.createClasses()); } } }
-
如上所示,在 Controller 层里,我们编写了一个 nonheap() 方法,在其内部无限循环地往 classList 中添加 Class 对象,直至内存溢出 (为了快速达到效果,这里我们将元空间大小 (-XX:MetaspaceSize) 和最大元空间大小 (-XX:MaxMetaspaceSize) 都设置为 32 M)
-
运行项目后,我们通过浏览器来调用该接口,直至内存溢出
三、导出内存映像文件
-
使用 -XX 命令自动导出
-
通过
-XX:+HeapDumpOnOutOfMemoryError
或-XX:HeapDumpPath
命令来进行导出-
-XX:+HeapDumpOnOutOfMemoryError
- 当发生内存溢出时,自动导出堆转储快照 (dump 文件)
-
-XX:HeapDumpPath
- 当发生内存溢出时,导出堆转储快照 (dump 文件) 至指定路径
-
当内存较大时,自动导出可能会失败
-
-
-
使用 jmap 命令手动导出
- 通过
jmap -dump:format=b,file=heap.hprof <pid>
命令来进行导出
- 通过
-
这里我们针对堆内存溢出,采用第二种方式来手动导出堆转储快照 (dump 文件)
四、使用 MAT 分析内存溢出
-
首先我们需要先下载一个 MAT (Eclipse Memory Analyzer Open Source)
-
下载好之后,启动 MAT,并打开之前生成的 heap.hprof 文件,生成内存溢出分析报告
- 如上所示,报告显示,a 区域占用了 93.57% 的内存,很明显的存在问题,我们可以点击 Details 来查看详细信息
-
这里我们不通过点击 Details 来分析,而是点击左上角的第二个图标
-
如上所示,通过输入包名来查找我们自己定义的类,我们可以很清晰地看到存在 769575 个 User 对象,占用了将近 100 M 内存,很明显存在问题
属性 含义 Class Name 类名 Objects 类的对象数量 Shallow Heap 当前对象本身占据的内存大小,不包含其引用的对象 Retained Heap 当前对象本身占据的内存大小 + 当前对象可直接或间接引用到的对象占据的内存大小
-
-
接着我们右击 User,来查看是谁引用了 User (只看强引用)
-
进入页面之后,我们一级一级地往下展开,最后看到了我们熟悉的 userList 和 User
-
至此,我们就定位到了内存溢出发生的地方,不过在实际生产环境中,造成内存溢出的问题可能会相对复杂,需要进行仔细分析才能解决