准备工作

首先创建一个项目,使用 https://start.spring.io/ 创建一个 spring boot 项目,要求:

  • spring boot 版本 :2.0.2.RELEASE防止版本不同,看到的实现不同。
  • 依赖:Web 模块

也就是说是一个简单的 Spring boot Web 项目

制造堆内存溢出

思路很简单,我们通过参数限制堆内存大小,然后不断创建新的对象来达到内存溢出

代码

  1. @RestController
  2. public class MemoryController {
  3. private List<Object> objects = new ArrayList<>();
  4. /**
  5. * 堆内存溢出
  6. * <pre>
  7. * 为了更快的看到效果,限制最大和最小内存:
  8. * -Xmx32M -Xms32M
  9. * 记得需要在启动的时候添加启动参数
  10. * </pre>
  11. */
  12. @GetMapping("/heap")
  13. public void heap() {
  14. while (true) {
  15. objects.add(UUID.randomUUID().toString());
  16. }
  17. }
  18. }

配置虚拟机参数

为了更快的看到效果,限制最大和最小内存:-Xmx32M -Xms32M
image.png

访问

然后访问这个地址

  1. http://localhost:8080/heap

控制台不一会就出现内存溢出了,如下
image.png

制造非堆内存溢出

非堆内存也就是 Metaspace 区,我们可以往里面存储 Class 信息,来达到这个效果。

引入依赖

动态生成 Class 的方式有很多种,这里使用 asm 来生成,先引入依赖

  1. <dependency>
  2. <groupId>asm</groupId>
  3. <artifactId>asm</artifactId>
  4. <version>3.3.1</version>
  5. </dependency>

使用 asm 来动态生成不同的类

  1. /***
  2. * https://blog.csdn.net/bolg_hero/article/details/78189621
  3. * 继承 ClassLoader 是为了方便调用 defineClass 方法,因为该方法的定义为 protected
  4. *
  5. * @author :anin
  6. * @date :Created in 2021/11/24 15:20
  7. */
  8. public class Metaspace extends ClassLoader {
  9. public static List<Class<?>> createClasses() {
  10. // 类持有
  11. List<Class<?>> classes = new ArrayList<>();
  12. // 循环 1000w 次生成 1000w 个不同的类。
  13. for (int i = 0; i < 10000000; ++i) {
  14. ClassWriter cw = new ClassWriter(0);
  15. // 定义一个类名称为 Class{i},它的访问域为 public,父类为 java.lang.Object,不实现任何接口
  16. cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
  17. "java/lang/Object", null);
  18. // 定义构造函数 <init> 方法
  19. MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
  20. "()V", null, null);
  21. // 第一个指令为加载 this
  22. mw.visitVarInsn(Opcodes.ALOAD, 0);
  23. // 第二个指令为调用父类 Object 的构造函数
  24. mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
  25. "<init>", "()V");
  26. // 第三条指令为 return
  27. mw.visitInsn(Opcodes.RETURN);
  28. mw.visitMaxs(1, 1);
  29. mw.visitEnd();
  30. Metaspace test = new Metaspace();
  31. byte[] code = cw.toByteArray();
  32. // 定义类
  33. Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
  34. classes.add(exampleClass);
  35. }
  36. return classes;
  37. }
  38. }

上面的 asm 使用不是本课程重点,了解即可。
制造非堆内存溢出

  1. private List<Class> classList = new ArrayList<>();
  2. /**
  3. * 非堆内存溢出
  4. * <pre>
  5. * 为了更快的看到效果,限制非堆最大和最小内存:
  6. * -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
  7. * 记得需要在启动的时候添加启动参数
  8. * </pre>
  9. */
  10. @GetMapping("/nonheap")
  11. public void nonheap() {
  12. while (true) {
  13. // 持有创建好的类是为了防止被垃圾回收器回收掉
  14. classList.addAll(Metaspace.createClasses());
  15. }
  16. }

访问

  1. http://localhost:8080/nonheap

控制台输出如图
image.png

导出内存映射文件

上面演示了两种内存溢出,如何来解决这一的问题?我们一般通过 分析内存映像文件 来找出到底是 哪些类一直占用没有被释放?,内存溢出有可能是内存泄露,也有可能是内存 CPU 不足
假如是内存泄露:我们需要找到是哪个地方导致的没有被释放?

  • C 语言中的内存泄露指的是:new 了一个对象,你把这个对象指针丢了,这块内存就永远得不到释放了
  • 而 Java 中的内存泄露指的是:new 了一个对象,被一直持有,得不到释放。

他们两个刚好是相反的。 :::warning 注意:我们这里首先导出的是 heap 堆内存溢出的内存映像文件,导出这个的时候就不要添加非堆的参数限制,否则很有可能非堆限制先异常。 :::

内存溢出自动导出

我们不可能一直去监控内存信息,一般是当发生内存溢出的时候,自动导出 这些信息以供我们查询,可以通过如下参数达到

  • -XX:+HeapDumpOnOutOfMemoryError:启用内存溢出自动导出功能
  • -XX:HeapDumpPath=./:配置导出的文件存放在哪里

在程序启动时添加到 jvm 参数,然后去重现内存溢出的场景,就能看到导出的文件了,如下
image.png
导出了一个java_pid43040.hprof 的文件
image.png

使用jmap命令手动导出

还有一种是手动方式使用 jmap工具导出,对于我们随时都能获取 heap 信息非常方便。

它的语法如下 jmap [ options ] pid jmap [ options ] executable core jmap [ options ] [ pid ] server-id@ ] remote-hostname-or-IP jdk工具之jmap(java memory map)

下面使用它导出我们上面启动的程序

  1. # 先找到我们要导出的程序
  2. $ jps
  3. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
  4. 43712 RemoteMavenServer36
  5. 26584 Launcher
  6. 12268 Jps
  7. 35868
  8. 43404 MonitorJvmDemoApplication
  9. # 使用 jmap 导出到文件中
  10. $ jmap -dump:format=b,file=heap.hprof 43404
  11. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
  12. Dumping heap to E:\anin\study\monitor-jvm-demo\heap.hprof ...
  13. Heap dump file created
  14. # 如果上面演示自动导出信息文件后,你对程序没有访问过,又用 jmap 导出了,他们的文件大小是差不多的

MAT 分析内存溢出

内存分析器(MAT) 是内存分析工具
比如可以使用 Eclipse Memory Analyzer是一种快速且功能丰富的 Java 堆分析器,可帮助您查找内存泄漏并减少内存消耗。

安装 MAT 工具

下载地址:http://www.eclipse.org/mat/downloads.php
这里的MAT工具需要和JDK工具对应。
我下的是 MAT8.1 (M1的mac没找到对应的包,下的不能用 = =!!!)

开始分析

使用 mat 工具打开我们导出的文件。
image.png
选择下面的一个常见报告(这里选择的只是默认打开的报告):

  • 怀疑泄漏的报告: Leak Suspects Report 自动检查堆转储是否存在泄漏疑点。报告保存了哪些对象活着,为什么他们不被垃圾收集
  • 组件报告:Component Report 分析一组对象以发现可疑的内存问题:重复的字符串,空集合、终结器、弱引用等
  • 重新打开先前运行的报告:Re-open previously run reports 现有报告存储在堆转储旁边的ZiP文件中

我们这里选择「怀疑泄露的报告」
image.png
这里饼图代表内存疑点,a区和b区有可能内存泄漏,a区疑点更大
image.png

  • A 区的描述:MemoryController这个实例类,占用了 58.56% 的内存,一下就定位到了我们的问题发生地区
  • B 区的描述:有 4734个实例是 Class,是被 system class loader 加载的,占用了 17.10% 的内存。 这里是正常的

    结果

    显而易见,MemoryController这个实例类里有一个数组占用内存过高,结合代码即可排查出问题。

    工具功能介绍

    image.png

  • Actions

    • Histogram:柱状图列出每个类的实例数
    • Dominator Tree:统治者树列出最大的物体和它们存活的东西。
    • Top Consumers:顶级消费者打印按类和包分组的最昂贵的对象。
    • Duplicate Classes:重复的类检测由多个类装入器装入的类。
  • Reports:报告
    • Leak Suspects:怀疑泄露包括泄漏疑点和系统概述
    • Top Components:顶部组件 列出大于堆总数 1% 的组件的报告
    • Leak Suspects by Snapshot Comparison:通过快照比较来怀疑泄漏 包括泄漏疑点和比较两个快照的系统概述。
  • Step By Step:一步一步?

    • Component Report:组件的报告分析属于普通根包或类装入器的对象

      Histogram

      以柱状图的形式 列出每个类的实例数
      image.png
      这个上面可以通过正则去筛选自己关注的类信息,正常情况下,我们可以筛选我们的包名等。
  • Class Name:类名

  • Objects:对象个数
  • Shallow Heap:直译就是浅层堆,其实就是这个对象实际占用的堆大小。这个比较难理解,有兴趣可以去查阅相关资料
  • Retained Heap:直译过来是保留堆,一般会大于或者等于shallow heap如果这个对象被删除了(GC 回收掉),能节省出多少内存,我们一般看这个值

我们可以看到我们的char[]占用的空间比较大,我们可以看该类是通过哪一个 GC ROOT 引用的,这里选择了排除软引用,只看强引用
image.png
image.png
通过上图可以看到:tomcat 的一个线程引用了 MemoryController,里面有一个 object的 ArrayList,该 list 中有 13万多个 object实例,每个实例的占用内存大小为 112byte
另外选中其中的类,还能看到他的一些属性,比如这个 object中的一个实例
image.png

Dominator Tree

以对象栈的方式查看,可以看到和上面的类似
image.png

小结

主要常用的两个模块是:

  • Histogram
  • Dominator Tree

首先定位可能性大的类,然后查看他的强引用信息,最后去定位。这里由于代码简单,很容易,在生产环境中可能就很复杂,需要使用好这个工具去查找

IDEA分析dump文件

通过 eclipse 的 mat 分析工具,可以看到很详细的图标之类的,就是安装上稍微有点麻烦,idea 其实也自带分析工具,如下图,直接打开 .hprof 文件即可 :::info 亲测2019版的IDEA打不开,2021版的可以打开 ::: image.png
可以看到上图显示的一些信息与我们所学的两个常用模块是很类似的。所以直接看这个分析工具也是可以的。