欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java内存泄漏总结

程序员文章站 2022-07-15 14:38:53
...

这一篇主要是对内存泄漏及其相关涉及到的知识进行介绍,以及内存泄漏的原因以及一些常见的场景进行介绍。
关于如何避免内存泄漏,后面将专门针对如何避免android的内存泄漏方法进行阐述,敬请期待~

分清“内存泄漏”与“内存溢出”

这两者名字比较像,而且有相似之处和交集的地方,两者都有可能造成OOM,但是原理是不同的。

内存泄露

程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。

内存溢出

内存溢出就是指内存越界。程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现oom。

Java运行环境内存结构

接下来讲讲Java的内存结构。
Java在JVM的虚拟内存环境中运行,JVM的内存分为三个区域:

堆(heap)

堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。

栈(stack)

栈最显著的特征是:LIFO(Last In, First Out, 后进先出)。栈中只存放基本类型和对象的引用(不是对象)。

方法区(method)

又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

有的时候将堆和方法区放在一块讲,因此简单理解的话,也可以将内存结构分为堆和栈两个部分。

结论

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

内存泄漏的原因

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,但有时也会很严重,会提示你Out of memory。
Java内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

接下来,我们再回顾一个跟内存泄漏涉及到的知识点——对象引用。因为前面讲了很多引用这个词,说明对象引用在内存泄漏上起到了重要的关键因素。
Java的对象引用分为4种:强引用,软引用,弱引用,虚引用,其中强引用是我们最常见的一种方式。比如

Object a = new Object();

4种引用的定义网上很多,在这不赘述,下面图主要讲了四种引用方式的区别:
Java内存泄漏总结
可以看出,不同的引用方式能够影响GC的回收,同时能看到,强引用声明的对象,只要保持引用状态,就会一直不会被回收,直到程序终止。所以如果当对象引用不合理,就会引发内存泄漏。

Java内存泄漏的常见场景

静态集合类引起内存泄漏

像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。
而且,当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
比如:

Set<Person> set = new HashSet<Person>();
  Person p1 = new Person("唐僧", "pwd1", 25);
  Person p2 = new Person("孙悟空", "pwd2", 26);
  Person p3 = new Person("猪八戒", "pwd3", 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);
  }

监听器

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

各种Connection连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC回收的。

内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。因此尽量使用静态内部类来避免内存泄漏的情况。(后面会专门针对Java内部类的文章)

单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在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类的对象将不能被回收。

总结

内存泄漏主要是在堆内存中,对象用完了之后,没有及时被GC回收。其中涉及到对象引用的情况,比如长生命周期的对象引用了短生命周期的对象,导致短生命周期对象用完之后仍然不能被回收。

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

1 采用良好的设计模式,能够避免一些设计上的纰漏,比如合理利用单例模式;
2 Connection,以及listener要记得及时关闭;
3 采用前人填坑的一些约定俗成的方法,少走弯路。比如用静态内部类代替内部类,并且采用软引用的方法判空的方法能够避免内部类对外部类的引用导致内存泄漏;

参考

Java 内存溢出 (OOM) 异常完全指南
Java内存、Android 内存泄漏
Java中的内存泄漏