Java 内存泄漏
前言:
Java语言的一个关键的优势就是它的内存管理机制。你只管创建对象,Java的垃圾回收器帮你分配以及回收内存。然而,实际的情况并没有那么简单,因为内存泄漏在Java应用程序中还是时有发生的。
1. 什么是内存泄漏?
创建的对象不再被其他应用程序使用,但因为被其他对象所引用着(即通过可达性分析,从GC Roots具有到该对象的链路),因此垃圾回收器没办法回收它们。
2. 为什么会发生内存泄漏?
下面这个例子中,A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。B对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间。
image.png
3. 常见发生内存泄漏情况
第二节说到过,由于某些生命周期长的对象引用了生命周期短的对象,而此时生命周期短的对象并没有被任何程序使用,依据jvm虚拟机规范,这些没有被任何程序使用的对象按理应该被垃圾收集器进行回收,但由于存在引用,因此没办法进行回收。最常见的情景便是静态集合类。
<1>静态集合(全局集合)
在使用Set、Vector、HashMap等集合类的时候需要特别注意,有可能会发生内存泄漏。当这些集合被定义成静态的时候,由于它们的生命周期跟应用程序一样长,此时,若往静态集合类中存放创建的java对象时,很可能发生内存泄漏。示例代码:
package com.lm.jvm;
import java.util.HashSet;
import java.util.Set;
/**
* @author lm
* @create 2018-10-12 21:28
* @desc java内存泄漏1:静态集合类
**/
public class MemoryLeak {
static Set<Object> set = new HashSet<>();
int size;
public void initSet(){
for (int i = 0; i < size; i++) {
Object o = new Object();
set.add(o);
o = null;
}
}
}
如上图代码所示,循环创建了Object对象,并添加到静态集合Set中,虽然将对象设置为null(不再使用),但由于静态成员变量生命周期与类的生命周期一致,即生命周期长的对象引用着不再被任何程序使用的生命周期短的对象,因此这些本该要被回收的对象并不能被GC,因此造成了内存泄漏。
<2>监听器
在Java中,我们经常会使用到监听器,如对某个控件添加单击监听器addOnClickListener()
,但往往释放对象的时候会忘记删除监听器,这就有可能造成内存泄漏。好的方法就是,在释放对象的时候,应该记住释放所有监听器,这就能避免了因为监听器而导致的内存泄漏。
<3>各种连接
Java中的连接包括数据库连接、网络连接和IO
连接,如果没有显式调用其close()
方法,是不会自动关闭的,这些连接就不能被GC回收而导致内存泄漏。一般情况下,在try
代码块里创建连接,在finally
里释放连接,就能够避免此类内存泄漏。
<4>外部模块的引用
调用外部模块的时候,也应该注意防止内存泄漏。如模块A调用了外部模块B的一个方法,如:
public void register(Object o)
这个方法有可能就使得A模块持有传入对象的引用,这时候需要查看B模块是否提供了去除引用的方法,如unregister()
。这种情况容易忽略,而且发生了内存泄漏的话,比较难察觉,应该在编写代码过程中就应该注意此类问题。
<5> 单例模式
使用单例模式的时候也有可能导致内存泄漏。因为单例对象初始化后将在JVM的整个生命周期内存在,如果它持有一个外部对象(生命周期比较短)的引用,那么这个外部对象就不能被回收,而导致内存泄漏。如果这个外部对象还持有其它对象的引用,那么内存泄漏会更严重,因此需要特别注意此类情况。这种情况就需要考虑下单例模式的设计会不会有问题,应该怎样保证不会产生内存泄漏问题。
<6>缓存
缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡。
常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存。这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
<7> 类装载器
Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。