jvm

内存泄漏

March 26, 2022

内存泄漏的原因

一般有以下几种:

  1. static字段,尤其是static的集合类型

    static字段的生命周期和jvm的生命周期一样长,随着类的加载被加载,存放于方法区内,只要类没有被卸载,就不会被gc回收。

    所以对于static字段的使用应该最小化,尤其是大容量的集合类型,对于不需要全生命周期的对象使用完毕以后应该及时清理或移除。


  2. 未关闭的连接,如数据库连接对象(connection、statement、resultset等)、IO流、socket等 连接也是要占用内存的,使用完忘了关闭的话,日积月累也会造成内存泄漏。


  3. 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不变。


  4. ThreadLocal


  5. 变量作用域大于其使用范围

    如下所示:

    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应该定义成局部变量。


  6. 缓存

    有时候为了加速计算,会缓存一些对象。一直需要用的或者有可能未来会用到的的缓存可以保留。

    但短生命周期的缓存积累比较多而又忘记清理的话,就可能发生内存泄漏。本质上还是长生命周期对象持有短生命周期对象(缓存中的对象)。

    解决办法是正确定义作用域,如上一点所述。

    或者使用WeakHashMap。




如何定位解决内存泄露?

严重的内存泄漏往往伴随着频繁的full gc、cpu使用率飙升、长时间运行后性能下降、outofmemory错误等情况,可以从这几个方面去定位问题。

通过各种工具,包括jdk自带的jstat或其他第三方监测工具如arthas,或者是通过分析dump日志(jvisualVM、JProfiler、MAT、GCViewer等),监测gc日志,如果出现频繁的full gc,而young gc反而较少,则很有可能出现内存泄漏。

接下来使用jmap查看每个类的对象数量及占用内存大小,找到占用空间最大、数量最多的对象,可以多监测几次,看其是否呈上升趋势。再结合对cpu使用率高的线程、出现oom错误的线程做线程堆栈分析,基本上就可以定位到代码层面了,然后就是具体的代码逻辑分析了,查找引起内存泄露的原因(参考上一节)。