内存泄漏和内存溢出
内存泄漏 memory leak
是指程序在申请内存后,无法释放已申请的内存空间 会发生内存泄漏,内存泄漏积累会导致内存溢出。
分类:
1、常发性内存泄露。发生内存泄露的代码被多次执行,每执行一次都会导致一快内存泄露。
2、偶发性内存泄露。发生内存泄露的代码只有在特定的环境下使用时才会发生内存泄露。
3、一次性内存泄露。发生泄露的代码 只会被执行一次 , 或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏 。
4、隐式内存泄漏。 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。(对于服务器程序而言的,因为服务器程序需要长期保持运行状态,不停的分配内存,也可能使得内存耗尽)
Java中引起内存泄漏的根本原因:
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收 。具体有以下几类:
1、静态集合类引起的内存泄漏:
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; }
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
2、当使用hash值的集合(HashSet,HashMap…)里面的对象属性被修改以后,再调用remove()方法时不起作用
public static void main(String[] args) { Set<Person> set = new HashSet<Person>(); Person p1 = new Person("张三","男",25); Person p2 = new Person("李四","男",26); Person p3 = new Person("王五","女",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("修改属性前总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变,但是HashSet里面对原来p3的引用的哈希值不变 System.out.println("修改属性后总共有:"+set.size()+" 个元素!"); set.remove(p3); //因为p3已经变成了新的对象,所以hashCode发生改变 //再通过p3来remove对象的时候,已经无法通过p3获取原来的hashCode // 也就造成此时原来的p3,remove不掉,造成内存泄漏 System.out.println("修改属性并执行remove()后总共有:"+set.size()+" 个元素!"); set.add(p3); //重新添加,此时的p3是新的hashCode,虽然与原来的p3还是同一个对象,但是hashCode不同 //所以p3可以再次加入到set集合中 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set) { System.out.println(person); } set.add(p3); //再次重新添加,添加不成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set) { System.out.println(person); } }
结果:
修改属性前总共有:3 个元素! 修改属性后总共有:3 个元素! 修改属性并执行remove()后总共有:3 个元素! 总共有:4 个元素! Person{name='李四', gender='男', age=26} Person{name='王五', gender='女', age=2} Person{name='张三', gender='男', age=25} Person{name='王五', gender='女', age=2} 总共有:4 个元素! Person{name='李四', gender='男', age=26} Person{name='王五', gender='女', age=2} Person{name='张三', gender='男', age=25} Person{name='王五', gender='女', age=2} Process finished with exit code 0
分析:
产生内存泄漏的原因是修改了p3的属性之后,p3指向的对象发生了改变,此时则会产生一个新的hashCode,无法通过现在的p3的hashCode再对原来的p3的进行操作,但是集合还保留着原来的p3对对象的引用,于是就发生了内存泄露。
修改属性后remove无效的原因是,此时的p3对象已经发生改变,产生了新的hashCode,无法通过新的哈希值对原来的p3对象进行操作。
后面还能将p3存入到集合中的原因,修改属性后,p3会产生新的hashcode,加入集合中时,虽然p3始终是同一个对象的引用,但由于集合当前存储的是原来的p3的hashCode,所以会认为这个新的p3的hashCode之前没有存储过,所以可以存储新的p3.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmbEXGte-1603007006233)(C:\Users\41209\AppData\Roaming\Typora\typora-user-images\1595600893891.png)]
**解决方法:**在退出程序之前,将集合中的东西clear,置为null,再退出程序。
通过上面的案例可以知道,在集合的使用中不要轻易地中途修改属性。
内存溢出 out of memory
是指程序在申请内存时,没有足够的内存空间供其使用 , 比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
常见溢出原因:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 启动参数内存值设定的过小
解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况