在上一章中我们介绍了 JVM 运行时参数以及 jstat 指令相关内容:[JVM 教程与调优] 什么是 JVM 运行时参数?. 下面我们来介绍一下 jmap+MAT 内存溢出.
首先我们来介绍一下下 JVM 的内存结构.
JVM 内存结构介绍
从图中我们可以看到, JVM 的内存结构分为两大块. 一块叫堆区, 一块叫非堆区.
堆区又分为两大块, 一块 Young, 一块叫 Old.Young 区又分为 Survivor 区和 Eden 区. Survivor 区我们又分为 S0 与 S1. 可以结合下图进行理解
非堆区呢, 是属于我们操作系统的本地内存. 它是独立于我们堆区之外的. 它在 JDK1.8 里面有一个新的名字, 叫 Metaspace.Metaspace 里面还包含几个块, 其中有一块就是 CCS, 还有一块是 CodeCache. 当然, 在我们的 Metaspace 中还包含很多其他块, 这里就不做扩展了.
接下来, 我们来通过实战, 来更加深入的理解 JVM 结构, 以及出现 JVM 内存溢出的原因.
实战理解
我们通过 spring.start 快速来生成一个 springboot 项目.
如图, 我们快速的创建一个 springboot 项目, 并将其下载下来.
这里我使用 Eclipse, 小伙伴们也可以使用 IDEA 或者其他开发工具也是可以的.
这里我们使用的是 SpringBoot 工程, 如果有的小伙伴对 SpringBoot 还不太熟悉的, 可以上网找一些教程先学习了解一下.
堆内存溢出演示
那么我们如何来构建一个堆内存溢出呢? 其实很简单, 我们只要定义一个 List 对象, 然后通过一个循环不停的往 List 里面塞对象. 因为只要 Controller 不被回收, 那么它里面的成员变量也是不会被回收的. 这样就会导致 List 里面的对象越来越多, 占用的内存越来越大, 最后就把我们的内存撑爆了.
创建 User 对象
这里我们先创建一个 User 对象.
- /**
- *
- * <p>Title: User</p>
- * <p>Description: </p>
- * @author Coder 编程
- * @date 2020 年 3 月 29 日
- */
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class User {
- private int id;
- private String name;
- }
这里面 @Data,@AllArgsConstructor,@NoArgsConstructor 用的是 lombok 注解. 不会使用的小伙伴, 可以在网上查找相关资料学习一下.
创建 Controller 对象
接下来我们来创建一个 Controller 来不停的往 List 集合中塞对象.
- /**
- *
- * <p>Title: MemoryController</p>
- * <p>Description: </p>
- * @author Coder 编程
- * @date 2020 年 3 月 29 日
- */
- @RestController
- public class MemoryController {
- private List<User> userList = new ArrayList<User>();
- /**
- * -Xmx32M -Xms32M
- * */
- @GetMapping("/heap")
- public String heap() {
- int i=0;
- while(true) {
- userList.add(new User(i++, UUID.randomUUID().toString()));
- }
- }
- }
为了更快达到我们的效果, 我们来设置两个参数.
-Xmx32M -Xms32M
一个最大内存, 一个最小内存. 我们的堆就只有 32M, 这样就很容易溢出.
访问测试
启动时候设置内存参数.
记得选中我们的 Arguments, 在 JVM 参数中, 将我们的值设置进去. 最后点击 Run 运行起来.
然后我们在浏览器中请求:
http://localhost:8080/heap
我们再观察控制台打印:
通过打印结果, 我们可以看到堆内存溢出了.
注意:
这里我们测试的时候可以很简单的看出在哪里出现的问题, 但是在实际生产环境中并没有那么简单, 因此我们需要借助工具, 来定位这些问题. 后续我们来介绍一下.
非堆内存溢出演示
接下来我们来演示一下非堆内存溢出, 我们继续沿用上方代码.
非堆内存主要是 MataSpace, 那么我们如何构建一个非堆内存溢出呢?
我们知道 MataSpace 主要存一些 class,filed,method 等这些东西.
因此我们继续创建一个 List 集合, 不断的往集合里面塞 class. 只要 List 不被回收, 那么它里面的 class 也不会被回收. 不停的往里面加之后, 就会造成溢出. 也就是我们的 MataSpace 溢出了.
如何来动态生成一些 class 呢? 其实是有很多工具的, 比如说: asm
引入 asm 工具包
这里我们引入 asm jar 包.
- <dependency>
- <groupId>asm</groupId>
- <artifactId>asm</artifactId>
- <version>3.3.1</version>
- </dependency>
动态生成类文件
还需要创建动态生成的类文件, 这里我们就不做扩展介绍, 有兴趣的小伙伴可以自行到网上查阅.
- /**
- *
- * <p>Title: Metaspace</p>
- * <p>Description: https://blog.csdn.net/bolg_hero/article/details/78189621
- * 继承 ClassLoader 是为了方便调用 defineClass 方法, 因为该方法的定义为 protected</p>
- * @author Coder 编程
- * @date 2020 年 3 月 29 日
- */
- public class Metaspace extends ClassLoader {
- public static List<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);
- // 定义构造函数 < 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();
- Metaspace test = new Metaspace();
- byte[] code = cw.toByteArray();
- // 定义类
- Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
- classes.add(exampleClass);
- }
- return classes;
- }
- }
创建 Controller
接下来我们再原 Controller 新增一个方法 nonheap
- /**
- *
- * <p>Title: MemoryController</p>
- * <p>Description: </p>
- * @author Coder 编程
- * @date 2020 年 3 月 29 日
- */
- @RestController
- public class MemoryController {
- private List<User> userList = new ArrayList<User>();
- private List<Class<?>> classList = new ArrayList<Class<?>>();
- /**
- * -Xmx32M -Xms32M
- * */
- @GetMapping("/heap")
- public String heap() {
- int i=0;
- while(true) {
- userList.add(new User(i++, UUID.randomUUID().toString()));
- }
- }
- /**
- * -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
- * */
- @GetMapping("/nonheap")
- public String nonheap() {
- while(true) {
- classList.addAll(Metaspace.createClasses());
- }
- }
- }
访问测试
这里我们同样在启动的时候也要设置 Mataspace 的值大小.
-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
接着我们在浏览器中访问地址: localhost:8080/nonheap
以上我们就完成了对堆内存溢出以及非堆内存溢出的演示.
小插曲
在测试非堆内存溢出的时候, 出现了另外一个错误.
java.lang.IncompatibleClassChangeError: Found interface org.objectweb.asm.MethodVisitor, but class was expected
这个异常另外写在 java.lang.IncompatibleClassChangeError https://www.jianshu.com/p/a2bf3b5f01a5 , 小伙伴如果有遇到, 可尝试一下是否能够解决
如何查看线上堆内存溢出以及非堆内存溢出
我们主要查看线上的内存映像文件来查看到底是哪里发生了内存溢出.
发生内存溢出的主要原因:
1. 内存发生泄漏
2. 内存分配不足
假如发生内存泄漏的话, 我们就需要找到是哪个地方发生了内存泄漏, 一直占用内存没有释放.
下面我们来看一下如何来导出我们的内存映像文件.
主要有两种方式.
1. 内存溢出自动导出
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./
第一个参数表示: 当发生内存溢出的时候, 将内存溢出文件 Dump 出来.
第二个参数表示: Dump 出来的文件存放的目录.
2. 使用 jmap 命令手动导出
如果我们使用第一种命令, 在发送内存溢出的时候再去导出, 可能就有点晚了. 我们可以等程序运行起来一段时间后, 就可以使用 jmap 命令导出来进行分析.
演示内存溢出自动导出
我们需要用到两个命令参数.
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./
我们接着运行项目, 访问: localhost:8080/heap
查看一下打印结果.
可以看到, 当发生了内存溢出后. 输出了一个 java_pid3972.hprof 的文件.
在当前项目的当前文件中, 我们就可以找到该文件.
演示 jmap 命令
option:-heap,-clstats,-dump:,-F
参数都是什么意思呢?
live: 只导出存活的对象, 如果没有指定, 则全部导出
format: 导出文件的格式
file: 导入的文件
我们刚才的程序还没有关闭, 我们来看下程序的 pid 是多少.
输入: jps -l
我们将其文件导入到桌面中来, 输入命令
jmap -dump:format=b,file=heap.hprof 3972
最后的 3972 是程序的 pid. 最后可以看到导出完毕.
还有其他的命令参数, 小伙伴们可以去官网 jmap 指令查看如何使用. 这里就不做过多介绍.
下一章节我们将通过命令实战定位 JVM 发生死循环, 死锁问题.
推荐
[JVM 教程与调优] 什么是 JVM 运行时参数?
[JVM 教程与调优] 为什么要学习 JVM 虚拟机?
[JVM 教程与调优] JVM 都有哪些参数类型?
文末
文章收录至
- GitHub: https://github.com/CoderMerlin/coder-programming
- Gitee: https://gitee.com/573059382/coder-programming
来源: https://www.cnblogs.com/coder-programming/p/12612021.html