内存泄漏的原因
一般有以下几种:
-
static字段,尤其是static的集合类型
static字段的生命周期和jvm的生命周期一样长,随着类的加载被加载,存放于方法区内,只要类没有被卸载,就不会被gc回收。
所以对于static字段的使用应该最小化,尤其是大容量的集合类型,对于不需要全生命周期的对象使用完毕以后应该及时清理或移除。
-
未关闭的连接,如数据库连接对象(connection、statement、resultset等)、IO流、socket等 连接也是要占用内存的,使用完忘了关闭的话,日积月累也会造成内存泄漏。
-
key没有正确实现hashcode和equals方法
当用在hashmap或hashset里,key没有正确实现hashcode和equals方法,本应只有一个entry,结果却存入多个entry,这部分entry没有办法通过key get到,自然也没办法删除。
只要map没被回收,它们就不会被回收,久而久之也可能造成内存泄漏。
例子:如果一个被用作key的类没有自定义hashcode和equals方法,属性完全相同的两个对象会被认为是not equals(object的equals方法比较的是对象的地址),每次put都会生成一个新的entry,get的时候也是使用当前对象的地址去get,其他的entry就变成了孤儿对象。
ps:常使用String作为key,一是因为String是不可变类,二是内容相同即equal且hashcode不变。
-
ThreadLocal
-
变量作用域大于其使用范围
如下所示:
1 2 3 4 5 6 7 8 9
public class UsingRandom { private String msg; public void receiveMsg() { // 从网络中接收数据保存在msg中 readFromNet(); // 把msg保存到数据库中 saveDB(); } }
saveDB后msg就没用了,但是由于和对象生命周期一样,还不能回收。 正确做法是msg应该定义成局部变量。
-
缓存
有时候为了加速计算,会缓存一些对象。一直需要用的或者有可能未来会用到的的缓存可以保留。
但短生命周期的缓存积累比较多而又忘记清理的话,就可能发生内存泄漏。本质上还是长生命周期对象持有短生命周期对象(缓存中的对象)。
解决办法是正确定义作用域,如上一点所述。
或者使用WeakHashMap。
如何定位解决内存泄露?
严重的内存泄漏往往伴随着频繁的full gc、cpu使用率飙升、长时间运行后性能下降、outofmemory错误等情况,可以从这几个方面去定位问题。
通过各种工具,包括jdk自带的jstat或其他第三方监测工具如arthas,或者是通过分析dump日志(jvisualVM、JProfiler、MAT、GCViewer等),监测gc日志,如果出现频繁的full gc,而young gc反而较少,则很有可能出现内存泄漏。
接下来使用jmap查看每个类的对象数量及占用内存大小,找到占用空间最大、数量最多的对象,可以多监测几次,看其是否呈上升趋势。再结合对cpu使用率高的线程、出现oom错误的线程做线程堆栈分析,基本上就可以定位到代码层面了,然后就是具体的代码逻辑分析了,查找引起内存泄露的原因(参考上一节)。