文章目录
- 引言
- 什么是内存泄漏?
- 内存泄漏的原因
- 1、静态集合类引起内存泄漏
- 2、监听器
- 3、各种连接
- 4、内部类和外部模块的引用
- 5、单例模式
- 模拟内存泄漏
- 1.1 写一段内存泄漏的代码
- 1.2 打包jar部署到服务器
- 1.3 请求接口
- 二、确定频繁Full GC现象
- 1.1 查看Java进程ID
- 1.2 查看GC信息
- 1.3 查看内存中存活的对象情况
- 1.4、生成堆转储快照dump文件
- 1.5、可视化分析dump文件
- 1.5.1 导入dump文件
- 1.5.2 定位到代码
- 1.5.3 补充GC回收
- 三、常用 JVM 调优启动参数
引言
某个业务系统在一段时间突然变慢,我们怀疑是因为出现内存泄露问题导致的,于是踏上排查之路。
什么是内存泄漏?
内存泄露 Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
Memory Leak会最终会导致Out Of Memory!
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点
- 首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
- 其次,这些对象是无用的,即程序以后不会再使用这些对象。
内存泄漏的原因
以下四种场景可能会导致内存泄露
1、静态集合类引起内存泄漏
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
参考:模拟场景1
2、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如
addXXXListener()
等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。参考:xxx
3、各种连接
比如:数据库连接(
dataSourse.getConnection()
),网络连接(socket
)和io
连接,除非其显式的调用了其close()
方法将其连接关闭,否则是不会自动被GC 回收的。参考:xxx
4、内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。
参考:xxx
5、单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。参考:模拟场景五
模拟内存泄漏
1.1 写一段内存泄漏的代码
/**
* <p>
* 模拟场景1: 静态集合属性内存泄露导致OOM
* JVM启动参数 -Xms300M -Xmx300M -Xmn200M (设置为300M可以查看内存泄漏,但恰好又不会发生内存溢出)
* </p>
*/
@RestController
@RequestMapping("/memory")
public class MemoryLeakController {
//静态集合
public static List<Double> list = new ArrayList<>();
@GetMapping("/leak")
public void testMemoryLeak () throws InterruptedException {
Thread.sleep(10000);
System.out.println("Debug Point 1");
this.populateList();
System.out.println("Debug Point 3");
Thread.sleep(10000);
System.gc(); //显示触发Full GC (若是静态属性,则不起作用; 若是非静态属性,则内存马上回收)
Thread.sleep(Integer.MAX_VALUE); // 目的:让进程一直存在,不结束。
}
// 往list属性中循环添加指定的随机数
public void populateList() {
for (int i = 0; i < 10000000; i++) {
// Double 占用内存字节240000000(大约240000000/1024/1024 = 228M)
list.add(Math.random());
}
System.out.println("Debug Point 2");
}
}
总结
在MemoryLeakController
类中存在一个静态的list
,代码中循环添加Double
类,由于静态常量属于强引用不会被GC回收,最终结果就是导致内存一直被占用,即内存泄漏。
解决方法(针对集合)最好不要定义成静态属性,在代码逻辑运行完后,将大集合对象调用clear()方法清空内容, 如:
list.clear();
(不要直接设置为null)
/**
* <p>
* 模拟场景1: 单例模式内存泄露导致OOM
* JVM启动参数 -Xms30M -Xmx30M -Xmn20M (设置为30M可以查看内存泄漏,但恰好又不会发生内存溢出)
* </p>
*/
@RestController
@RequestMapping("")
public class TrashRecyslingController {
@Autowired
FinalClassVariables finalClassVariables;
/**
* 重新加载所有缓存
**/
@GetMapping("/")
public void testTrashRecys() {
finalClassVariables.setFinalVari();
}
}
@Component
public class FinalClassVariables {
final Map<String, TestBeanTrash> finalVari = new HashMap<>();
public void setFinalVari () {
for (int i = 0; i < 1000000; i++) {
String uuid = IdWorker.get32UUID();
finalVari.put(uuid, new TestBeanTrash(uuid));
}
}
}
总结
通过Java 中IOC 机制默认注入的对象都是单例的对象,在单例对象中声明成员变量时,如果变量是个能够无限自增扩容的数据结构时,需要注意及时释放这个对象,否则就会导致对象被无限扩容,最终导致内存泄漏;
解决方法:①将Bean生成方式改成非单例模式;
②在Map对象使用完毕之后将对象中的值清除掉;
③将Map对象降级成局部变量;
个人推荐使用第③种方式,写代码的人不同,很有可能漏掉①②步
1.2 打包jar部署到服务器
参数解释
nohup
表示不挂断的运行, 注意并没有后台运行的功能,用nohup
命令可以使命令永久的执行, 和客户端没有任何关系
&
表示是后台运行
-Xms20M -Xmx20M -Xmn10M
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
表示为了发生OOM的时候会自动导出Dump文件
-
nohup java -jar -Xms20M -Xmx20M -Xmn10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof &
输入此命令运行项目 -
nohup java -jar xxx.jar &
可以让jar包一直后台运行 -
java -jar -Xms300M -Xmx300M -Xmn100M ums-0.0.1-SNAPSHOT.jar
设置jvm参数并运行项目、
1.3 请求接口
部署之后请求一下接口: http://127.0.0.1:8088/memory/leak
二、确定频繁Full GC现象
说明:
- 在
window
环境操作的(与linux
没有区别 )- 进程ID 简称为
PID
1.1 查看Java进程ID
-
jps -l
显示当前所有 Java 进程ID 的命令 (进程ID为18441)
1.2 查看GC信息
参数解释:
gcutil
表示[已使用空间]占[总空间]的百分比。
1000
表示每多少毫秒查询一次。
2
表示每多少毫秒查询2次,若不指定,表示一直查。
-
jstat -gcutil 18441 1000 2
或jstat -gc 18441 1000 2
表示1000毫秒查询2次,监视虚拟机各种运行状态信息。
1.3 查看内存中存活的对象情况
若存活的数据不正常,十有八九就是泄露
-
jmap -heap 18441
查看进程的JVM
占用内存情况 -
jmap -histo:live 18441
显示堆中当前活动的所有对象的统计信息,按实例对象数量从高到低显示。重点关注实例对象数量过多的类。并找到对应程序。 -
jmap -histo 18441| head -n 10
查看前10的对象统计信息(仅用于linux
,window
执行不了)
可以看出Dobule的实例对象有100万,占用内存大约22.8M的样子。这肯定不正常。
下一步就是如何定位到代码?
1.4、生成堆转储快照dump文件
format=b
表示输出为二进制;heap.dump
表示输出的文件名为heap.dump
(可指定相对路径或绝对路径);pid
表示进程ID注意:使用
jmap
dump堆信息时,会触发Full GC, 触发Full GC 可能导致线上服务不可用,因此,要慎重使用,所以尽量不要在线上执行此命令。如果想dump堆信息,可以使用gcore
命令,比jmap -dump
快。
-
jmap -dump:format=b,file=heap.dump 18441
生成堆转储快照dump文件(即:导出堆信息) -
jmap -dump:format=b,file=heap.hprof 18441
生成堆转储快照dump文件(即:导出堆信息)
1.5、可视化分析dump文件
本地工具
IBM HeapAnalyzer
,mat
,jvisualvm
在线工具
https://fastthread.io/
,https://heaphero.io/
只演示利用eclipse插件MAT来分析heap profile。(官网可以下载,解压后直接使用)
1.5.1 导入dump文件
- 打开mat工具
- 点击 file -> Open Heap Dump,选择刚才的文件(
18441.dump
) - 等待加载完成,如下图
List 占用了空间的 43.9%,基本可以断定为是 List没有被回收导致的内存泄漏
1.5.2 定位到代码
查看代码中使用到 List 地方的代码即可定位。
1.5.3 补充GC回收
内存泄漏的原因分析,总结出来只有一条:存在无效的引用!良好的编码规范以及合理使用设计模式有助于解决此类问题。
三、常用 JVM 调优启动参数
-
-verbose:gc
输出每次GC
的相关情况 -
-verbose:jni
输出native方
法调用的相关情况,一般用于诊断jni调用错误信息 -
-Xms n
指定jvm
堆的初始大小,默认为物理内存的1/64,最小为1M;可以指定单位,比如k、m,若不指定,则默认为字节 -
-Xmx n
指定jvm
堆的最大值,默认为物理内存的1/4或者1G,最小为2M;单位与-Xms
一致 -
-Xss n
设置单个线程栈的大小,一般默认为512k -
-XX:NewRatio=4
设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 -
-Xmn
设置新生代内存大小。整个堆大小=年轻代大小 + 年老代大小 + 持久带大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 -
-XX:SurvivorRatio=4
设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -
-XX:MaxTenuringThreshold=0
设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率