首页 / JAVA  

内存溢出与内存泄漏解析

内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

内存泄漏(memory leak):内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能被GC回收,于是就发生了内存泄漏。

内存泄漏会最终会导致内存溢出!

内存溢出有以下几种常见的情况:

1、java.lang.OutOfMemoryError: PermGen space (持久带溢出)

     我们知道jvm通过持久带实现了java虚拟机规范中的方法区,而运行时常量池就是保存在方法区中的,因此发生这种溢出可能是运行时常量池溢出,或是由于程序中使用了大量的jar或class,使得方法区中保存的class对象没有被及时回收或者class信息占用的内存超过了配置的大小。

2、java.lang.OutOfMemoryError: Java heap space (堆溢出)

     发生这种溢出的原因一般是创建的对象太多,在进行垃圾回收之前对象数量达到了最大堆的容量限制。

     解决这个区域异常的方法一般是通过内存映像分析工具对Dump出来的堆转储快照进行分析,看到底是内存溢出还是内存泄漏。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,定位出泄漏代码的位置,修改程序或算法;如果不存在泄漏,就是说内存中的对象确实都还必须存活,那就应该检查虚拟机的堆参数-Xmx(最大堆大小)和-Xms(初始堆大小),与机器物理内存对比看是否可以调大。

3、虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

常见的内存泄漏场景?

1.静态集合类引起的内存泄漏:
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,从而造成内存泄漏,因为他们也将一直被Vector等引用着。
Vector<Object> v=new Vector<Object>(100);
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、修改HashSet中对象的参数值,且参数是计算哈希值的字段

     当一个对象被存储到HashSet集合中以后,修改了这个对象中那些参与计算哈希值的字段后,这个对象的哈希值与最初存储在集合中的就不同了,这种情况下,用contains方法在集合中检索对象是找不到的,这将会导致无法从HashSet中删除当前对象,造成内存泄漏,举例如下:
public static void main(String[] args){
    Set<Person> set = new HashSet<Person>();
    Person p1 = new Person("张三","1",25);
    Person p2 = new Person("李四","2",26);
    Person p3 = new Person("王五","3",27);
    set.add(p1);
    set.add(p2);
    set.add(p3);
    System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
    p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
    set.remove(p3); //此时remove不掉,造成内存泄漏
    set.add(p3); //重新添加,可以添加成功
    System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
    
    for (Person person : set){
        System.out.println(person);
    }
}

3、监听器 

     在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、各种连接

     比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close() 方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去连接,在finally里面释放连接。

5、单例模式

   如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。

  不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:

class A{
    public A(){
        B.getInstance().setA(this);
    }
    ....
}
//B类采用单例模式
class B{
    private A a;
    private static B instance=new B();
    public B(){}
    
    public static B getInstance(){
        return instance;
    }
    
    public void setA(A a){
        this.a=a;
    }
    //getter...
}

显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收,所以造成内存泄漏


避免内存泄漏的几点建议:

1、尽早释放无用对象的引用。

2、避免在循环中创建对象。

3、使用字符串处理时避免使用String,应使用StringBuffer。

4、尽量少使用静态变量,因为静态变量存放在永久代,基本不参与垃圾回收。

2019-10-29