如何解决内存泄漏问题?
关联课程内容
- 实战篇-内存泄漏和内存溢出
- …
- 实战篇-btrace和arthas在线定位问题
回答路径
- 内存泄漏和内存溢出
- 解决内存泄漏问题的思路
- 常用的工具
内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出。
解决内存泄漏问题总共分为四个步骤,其中前两个步骤是最核心的:
# 发现问题 – 堆内存状况的对比
正常情况
- 处理业务时会出现上下起伏,业务对象频繁创建内存会升高,触发MinorGC之后内存会降下来。
- 手动执行FULL GC之后,内存大小会骤降,而且每次降完之后的大小是接近的。
- 长时间观察内存曲线应该是在一个范围内。
出现内存泄漏
- 处于持续增长的情况,即使Minor GC也不能把大部分对象回收
- 手动FULL GC之后的内存量每一次都在增长
- 长时间观察内存曲线持续增长
生产环境通过运维提供的Prometheus + Grafana等监控平台查看
开发、测试环境通过visualvm查看
package q7oom;
import java.util.ArrayList;
import java.util.List;
//-Xmx10m -verbose:gc
public class OOMDemo {
private static int _1MB = 1024 * 1024 * 1;
public static void main(String[] args) throws InterruptedException {
List<Object> objects = new ArrayList<>();
while (true){
byte[] bytes = new byte[_1MB];
//强引用
objects.add(bytes);
Thread.sleep(50);
}
}
}
这段代码执行之后,使用visualvm查看结果:
处于持续增长的情况,手动FULL GC之后的内存量每一次都在增长,长时间观察内存曲线持续增长。属于内存泄漏的情况。
# 诊断 – 生成内存快照
当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。
生成方式有两种
1、内存溢出时自动生成,添加生成内存快照的Java虚拟机参数:
-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
-XX:HeapDumpPath=<path>:指定hprof文件的输出路径。
发生oom之后,就会生成内存快照文件:
2、导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:
通过JDK自带的jmap命令导出,格式为:
jmap -dump:live,format=b,file=文件路径和文件名 进程ID
通过arthas的heapdump命令导出,格式为:
heapdump --live 文件路径和文件名
# 诊断 – MAT定位问题
使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。
# 修复问题
修复内存溢出问题的要具体问题具体分析,问题总共可以分成三类:
- 代码中的内存泄漏,由于代码的不合理写法存在隐患,导致内存泄漏
- 并发引起内存溢出 - 参数不当,由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。解决方案:设置合理参数
- 并发引起内存溢出 – 设计不当,系统的方案设计不当,比如:
- 从数据库获取超大数据量的数据
- 线程池设计不当
- 生产者-消费者模型,消费者消费性能问题
解决方案:优化设计方案
# 常用的JVM工具
JDK自带的命令行工具:
jps 查看java进程,打印main方法所在类名和进程id
jmap 1、生成堆内存快照
2、打印类的直方图
第三方工具:
VisualVM 监控
Arthas 综合性工具
MAT 堆内存分析工具
监控工具:
Prometheus + grafana