准备工作
首先创建一个项目,使用 https://start.spring.io/ 创建一个 spring boot 项目,要求:
- spring boot 版本 :2.0.2.RELEASE防止版本不同,看到的实现不同。
- 依赖:Web 模块
制造堆内存溢出
思路很简单,我们通过参数限制堆内存大小,然后不断创建新的对象来达到内存溢出
代码
@RestController
public class MemoryController {
private List<Object> objects = new ArrayList<>();
/**
* 堆内存溢出
* <pre>
* 为了更快的看到效果,限制最大和最小内存:
* -Xmx32M -Xms32M
* 记得需要在启动的时候添加启动参数
* </pre>
*/
@GetMapping("/heap")
public void heap() {
while (true) {
objects.add(UUID.randomUUID().toString());
}
}
}
配置虚拟机参数
为了更快的看到效果,限制最大和最小内存:-Xmx32M -Xms32M
访问
然后访问这个地址
http://localhost:8080/heap
制造非堆内存溢出
非堆内存也就是 Metaspace 区,我们可以往里面存储 Class 信息,来达到这个效果。
引入依赖
动态生成 Class 的方式有很多种,这里使用 asm 来生成,先引入依赖
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
使用 asm 来动态生成不同的类
/***
* https://blog.csdn.net/bolg_hero/article/details/78189621
* 继承 ClassLoader 是为了方便调用 defineClass 方法,因为该方法的定义为 protected
*
* @author :anin
* @date :Created in 2021/11/24 15:20
*/
public class Metaspace 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();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
// 定义类
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
上面的 asm 使用不是本课程重点,了解即可。
制造非堆内存溢出
private List<Class> classList = new ArrayList<>();
/**
* 非堆内存溢出
* <pre>
* 为了更快的看到效果,限制非堆最大和最小内存:
* -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
* 记得需要在启动的时候添加启动参数
* </pre>
*/
@GetMapping("/nonheap")
public void nonheap() {
while (true) {
// 持有创建好的类是为了防止被垃圾回收器回收掉
classList.addAll(Metaspace.createClasses());
}
}
访问
http://localhost:8080/nonheap
导出内存映射文件
上面演示了两种内存溢出,如何来解决这一的问题?我们一般通过 分析内存映像文件 来找出到底是 哪些类一直占用没有被释放?,内存溢出有可能是内存泄露,也有可能是内存 CPU 不足
假如是内存泄露:我们需要找到是哪个地方导致的没有被释放?
- C 语言中的内存泄露指的是:new 了一个对象,你把这个对象指针丢了,这块内存就永远得不到释放了
- 而 Java 中的内存泄露指的是:new 了一个对象,被一直持有,得不到释放。
他们两个刚好是相反的。 :::warning 注意:我们这里首先导出的是 heap 堆内存溢出的内存映像文件,导出这个的时候就不要添加非堆的参数限制,否则很有可能非堆限制先异常。 :::
内存溢出自动导出
我们不可能一直去监控内存信息,一般是当发生内存溢出的时候,自动导出 这些信息以供我们查询,可以通过如下参数达到
-XX:+HeapDumpOnOutOfMemoryError
:启用内存溢出自动导出功能-XX:HeapDumpPath=./
:配置导出的文件存放在哪里
在程序启动时添加到 jvm 参数,然后去重现内存溢出的场景,就能看到导出的文件了,如下
导出了一个java_pid43040.hprof
的文件
使用jmap命令手动导出
还有一种是手动方式使用 jmap工具导出,对于我们随时都能获取 heap 信息非常方便。
它的语法如下 jmap [ options ] pid jmap [ options ] executable core jmap [ options ] [ pid ] server-id@ ] remote-hostname-or-IP jdk工具之jmap(java memory map)
下面使用它导出我们上面启动的程序
# 先找到我们要导出的程序
$ jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
43712 RemoteMavenServer36
26584 Launcher
12268 Jps
35868
43404 MonitorJvmDemoApplication
# 使用 jmap 导出到文件中
$ jmap -dump:format=b,file=heap.hprof 43404
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Dumping heap to E:\anin\study\monitor-jvm-demo\heap.hprof ...
Heap dump file created
# 如果上面演示自动导出信息文件后,你对程序没有访问过,又用 jmap 导出了,他们的文件大小是差不多的
MAT 分析内存溢出
内存分析器(MAT) 是内存分析工具
比如可以使用 Eclipse Memory Analyzer是一种快速且功能丰富的 Java 堆分析器,可帮助您查找内存泄漏并减少内存消耗。
安装 MAT 工具
下载地址:http://www.eclipse.org/mat/downloads.php
这里的MAT工具需要和JDK工具对应。
我下的是 MAT8.1 (M1的mac没找到对应的包,下的不能用 = =!!!)
开始分析
使用 mat 工具打开我们导出的文件。
选择下面的一个常见报告(这里选择的只是默认打开的报告):
- 怀疑泄漏的报告: Leak Suspects Report 自动检查堆转储是否存在泄漏疑点。报告保存了哪些对象活着,为什么他们不被垃圾收集
- 组件报告:Component Report 分析一组对象以发现可疑的内存问题:重复的字符串,空集合、终结器、弱引用等
- 重新打开先前运行的报告:Re-open previously run reports 现有报告存储在堆转储旁边的ZiP文件中
我们这里选择「怀疑泄露的报告」
这里饼图代表内存疑点,a区和b区有可能内存泄漏,a区疑点更大
- A 区的描述:
MemoryController
这个实例类,占用了 58.56% 的内存,一下就定位到了我们的问题发生地区 B 区的描述:有 4734个实例是 Class,是被
system class loader
加载的,占用了 17.10% 的内存。 这里是正常的结果
显而易见,
MemoryController
这个实例类里有一个数组占用内存过高,结合代码即可排查出问题。工具功能介绍
Actions
- Histogram:柱状图列出每个类的实例数
- Dominator Tree:统治者树列出最大的物体和它们存活的东西。
- Top Consumers:顶级消费者打印按类和包分组的最昂贵的对象。
- Duplicate Classes:重复的类检测由多个类装入器装入的类。
- Reports:报告
- Leak Suspects:怀疑泄露包括泄漏疑点和系统概述
- Top Components:顶部组件 列出大于堆总数 1% 的组件的报告
- Leak Suspects by Snapshot Comparison:通过快照比较来怀疑泄漏 包括泄漏疑点和比较两个快照的系统概述。
Step By Step:一步一步?
Class Name:类名
- Objects:对象个数
- Shallow Heap:直译就是浅层堆,其实就是这个对象实际占用的堆大小。这个比较难理解,有兴趣可以去查阅相关资料
- Retained Heap:直译过来是保留堆,一般会大于或者等于shallow heap如果这个对象被删除了(GC 回收掉),能节省出多少内存,我们一般看这个值
我们可以看到我们的char[]占用的空间比较大,我们可以看该类是通过哪一个 GC ROOT 引用的,这里选择了排除软引用,只看强引用
通过上图可以看到:tomcat 的一个线程引用了 MemoryController,里面有一个 object的 ArrayList,该 list 中有 13万多个 object实例,每个实例的占用内存大小为 112byte
另外选中其中的类,还能看到他的一些属性,比如这个 object中的一个实例
Dominator Tree
小结
主要常用的两个模块是:
- Histogram
- Dominator Tree
首先定位可能性大的类,然后查看他的强引用信息,最后去定位。这里由于代码简单,很容易,在生产环境中可能就很复杂,需要使用好这个工具去查找
IDEA分析dump文件
通过 eclipse 的 mat 分析工具,可以看到很详细的图标之类的,就是安装上稍微有点麻烦,idea 其实也自带分析工具,如下图,直接打开 .hprof 文件即可
:::info
亲测2019版的IDEA打不开,2021版的可以打开
:::
可以看到上图显示的一些信息与我们所学的两个常用模块是很类似的。所以直接看这个分析工具也是可以的。