聊聊内存泄露
1. 什么是内存泄露
看到网上有很多人都在问内存泄露与内存溢出的区别,而且后面还有一大堆的跟帖在用不同形式的语言予以解答,我看了以后思绪万千啊。内存泄露是导致内存溢出的原因之一,说他们的区别纯属无稽之谈。要解释什么是内存泄露还真是个费事的活,我用一个例子来解释下:
public class Test { public static void main(String[] args) { List<String> list = new ArrayList<String>(); while (true) { String test = new String("111"); list.add(test); } } }
上面的代码会不停的往list中添加数据,当我们的堆空间不足时,就会报OOM的错误,如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.leak.Test.main(Test.java:11)
当堆空间不足的时候,JVM会进行垃圾回收,但是垃圾回收时却发现这些对象都是有用的,不能回收。我们可能会考虑增大堆空间大小,可是这还是于是无补,因为我们有个死循环。这种情况其实就是内存泄露的一个简单例子,简而言之,如果是内存泄露引起的OOM,那肯定是我们代码有问题,需要修正代码。
2. 如何确定OOM是由于内存泄露引起的
在工作中,遇到OOM,你首先要确定他是由于什么原因引起的?是因为堆空间设置太小引起还是因为内存泄露引起。实际上,内存泄露的问题可以通过增大堆空间暂时得到解决,但是他不是长久之计。
我们可以通过对应用访问峰值时堆空间利用率的分析来确定应用是否存在内存泄露,比如我们可以用JMeter来进行压力测试,我们每次对应用加压1000,一共加压10次,第一次峰值时堆使用了100M,第二次峰值时使用了200M,第三次峰值时使用了300M….那这样我们基本可以确定应用存在内存泄露。因为正常情况下,每次峰值时的堆占用率应该是差不多的,而上面的例子每次峰值时数据出入都比较大,而且是逐步增加,这不是一个正常的现象。
观察内存的使用情况,你可以使用JConsole或者VisualVM等工具,我比较喜欢从GC的日志中得到我想要的信息,每次峰值时由于堆空间吃紧,肯定会触发一次GC,我通过这几次GC记录可以明了的看到堆内存情况。我们可以通过配置JVM参数来启用一些基本的GC日志,比如-verbose:gc、-XX:+PrintGCTimeStamps、-XX:+PrintGCDetails、-Xloggc:<file>。至于如何读GC日志,我博客里有其他文章讲解,Oracle官网也有比较好的例子。
总结一下,如何确定应用存在内存泄露问题,我们需要观察峰值时的堆内存变化,比如堆的使用情况像下图一样,那肯定是存在Memory Leak了。
3. 如何定位引起内存泄露的代码
首先我们可以看发生OOM时的代码,比如上面的例子,我们大概可以知道在执行哪段代码时发生了错误,然后重点看下这部分代码。当然,那部分代码不一定就是导致OOM的代码。
接下来我们需要分析堆快照,可以为JVM配置发生OOM时出生堆快照文件(+XX:+HeapDumpOnOutOfMemoryError),或者使用jmap命令产生。注意生成堆快照文件时应用会停止运行,所以千万不要在生产环境中这么搞。
拿到堆快照文件后,我们使用Mat或者VisualVM工具进行分析。借助这些工具,我们可以根据实例数、占用大小对目前堆中的所有实例进行排序,那排在前几位的就是你要重点分析的。
前面讲的方法很容易就能找出大范围的Memory Leak代码,但是对于一些小的内存溢出问题,我们可能就比较难发现了,我的经验是先定位是哪些功能点引起的内存泄露,然后重点去压这部分功能,放大他们的影响之后再去分析。