容易发生内存泄露的八个场景,你都知道吗?
内存泄漏与内存溢出
JVM在运行时会存在大量的对象,一部分对象是长久使用的,一部分对象只会短暂使用
JVM会通过可达性分析算法和一些条件判断对象是否再使用,当对象不再使用时,通过GC将这些对象进行回收,避免资源被用尽
内存泄漏:当不再需要使用的对象,因为不正确使用时,可能导致GC无法回收这些对象
当不正确的使用导致对象生命周期变成也是宽泛意义上的内存泄漏
内存溢出:当大量内存泄漏时,可能没有资源为新对象分配举例内存泄漏
接下来将从对象生命周期变长、不关闭资源、改变对象哈希值、缓存等多个场景举例内存泄漏对象生命周期变长引发内存泄漏静态集合类publicclassStaticClass{privatestaticfinalListObjectlistnewArrayList();尽管这个局部变量Object生命周期非常短但是它被生命周期非常长的静态列表引用所以不会被GC回收发生内存溢出publicvoidaddObject(){ObjectonewObject();list。add(o);}}
类卸载的条件非常苛刻,这个静态列表生命周期基本与JVM一样长
静态集合引用局部对象,使得局部对象生命周期变长,发生内存泄漏饿汉式单例模式publicclassSingleton{privatestaticfinalSingletonINSTANCEnewSingleton();privateSingleton(){if(INSTANCE!null){thrownewRuntimeException(notcreateinstance);}}publicstaticSingletongetInstance(){returnINSTANCE;}}
饿汉式的单例模式也是被静态变量引用,即时不需要使用这个单例对象,GC也不会回收非静态内部类
非静态内部类会有一个指针指向外部类publicclassInnerClassTest{classInnerClass{}publicInnerClassgetInnerInstance(){returnthis。newInnerClass();}publicstaticvoidmain(String〔〕args){InnerClassinnerInstancenull;{InnerClassTestinnerClassTestnewInnerClassTest();innerInstanceinnerClassTest。getInnerInstance();System。out。println(外部实例对象内存布局);System。out。println(ClassLayout。parseInstance(innerClassTest)。toPrintable());System。out。println(内部实例对象内存布局);System。out。println(ClassLayout。parseInstance(innerInstance)。toPrintable());}省略很多代码。。。。。}}
当调用外部类实例方法通过外部实例对象返回一个内部实例对象时(调用代码中的getInnerInstance方法)
外部实例对象不需要使用了,但内部实例对象被长期使用,会导致这个外部实例对象生命周期变长
因为内部实例对象隐藏了一个指针指向(引用)创建它的外部实例对象
实例变量作用域不合理
如果只需要一个变量作为局部变量,在方法结束就不使用它了,但是把他设置为实例变量,此时如果该类的实例对象生命周期很长也会导致该变量无法回收发生内存泄漏(因为实例对象引用了它)
变量作用域设置的不合理会导致内存泄漏隐式内存泄漏
动态数组ArrayList中remove操作会改变size的同时将删除位置置空,从而不再引用元素,避免内存泄漏
不置空要删除的元素对数组的添加删除查询等操作毫无影响(看起来是正常的),只是会带来隐式内存泄漏不关闭资源引发内存泄漏
各种连接:数据库连接、网络连接、IO连接在使用后忘记关闭,GC无法回收它们,会发生内存泄漏
所以使用连接时要使用trywithresource自动关闭连接改变对象哈希值引发内存泄漏
一般认为对象逻辑相等,只要对象关键域相等即可
一个对象加入到散列表是通过计算该对象的哈希值,通过哈希算法得到放入到散列表哪个索引中
如果将对象存入散列表后,修改了该对象的关键域,就会改变对象哈希值,导致后续要在散列表中删除该对象,会找错索引从而找不到该对象导致删除失败(极小概率找得到)publicclassHashCodeTest{假设该对象实例变量a,d是关键域a,d分别相等的对象逻辑相等privateinta;privatedoubled;Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onullgetClass()!o。getClass())returnfalse;HashCodeTestthat(HashCodeTest)o;returnathat。aDouble。compare(that。d,d)0;}OverridepublicinthashCode(){returnObjects。hash(a,d);}publicHashCodeTest(inta,doubled){this。aa;this。dd;}publicHashCodeTest(){}OverridepublicStringtoString(){returnHashCodeTest{aa,dd};}publicstaticvoidmain(String〔〕args){HashMapHashCodeTest,IntegermapnewHashMap();HashCodeTesth1newHashCodeTest(1,1。5);map。put(h1,100);map。put(newHashCodeTest(2,2。5),200);修改关键域导致改变哈希值h1。a100;System。out。println(map。remove(h1));nullSetMap。EntryHashCodeTest,IntegerentrySetmap。entrySet();for(Map。EntryHashCodeTest,Integerentry:entrySet){System。out。println(entry);}HashCodeTest{a100,d1。5}100HashCodeTest{a2,d2。5}200}}
所以说对象当作Key存入散列表时,该对象最好是逻辑不可变对象,不能在外界改变它的关键域,从而无法改变哈希值
将关键域设置为final,只能在实例代码块中初始化或构造器中
如果关键域是引用类型,可以用final修饰后,对外不提供改变该引用关键域的方法,从而让外界无法修改引用关键域中的值(如同String类型,所以String常常用来当作散列表的Key)缓存引发内存泄漏
当缓存充当散列表的Key时,如果不再使用该缓存,就要手动在散列表中删除,否则会发生内存泄漏
如果使用的是WeakHashMap,它内部的Entry是弱引用,当它的Key不再使用时,下次垃圾回收就会回收掉,不会发生内存泄漏publicclassCacheTest{privatestaticMapString,StringweakHashMapnewWeakHashMap();privatestaticMapString,StringmapnewHashMap();publicstaticvoidmain(String〔〕args){模拟要缓存的对象Strings1newString(O1);Strings2newString(O2);weakHashMap。put(s1,S1);map。put(s2,S2);模拟不再使用缓存s1null;s2null;垃圾回收WeakHashMap中存的弱引用System。gc();try{TimeUnit。SECONDS。sleep(5);}catch(InterruptedExceptione){e。printStackTrace();}遍历各个散列表System。out。println(HashMap);traverseMaps(map);System。out。println();System。out。println(WeakHashMap);traverseMaps(weakHashMap);}privatestaticvoidtraverseMaps(MapString,Stringmap){for(Map。EntryString,Stringentry:map。entrySet()){System。out。println(entry);}}}
结果
注意:监听器和回调也应该像这样成为弱引用总结
这篇文章介绍内存泄漏与内存溢出的区别,并从生命周期变长、不关闭资源、改变哈希值、缓存等多方面举例内存泄漏的场景
内存泄漏是指当对象不再使用,但是GC无法回收该对象
内存溢出是指当大量对象内存泄漏,没有资源再给新对象分配
静态集合、饿汉单例、不合理的设置变量作用域都会使对象生命周期变长,从而导致内存泄漏
非静态内部对象有隐式指向外部对象的指针、使用集合不删除元素等都会隐式导致内存泄漏
忘记关闭资源导致内存泄漏(trywithresource自动关闭解决)
使用散列表时,充当Key对象的哈希值被改变导致内存泄漏(key使用逻辑不可变对象,关键域不能被修改)
缓存引发内存泄漏(使用弱引用解决)