# 基本概念

内存泄漏:程序无法释放已申请的内存空间且 GC 也无法回收,导致内存始终被占用。多次内存泄漏后堆积的结果就是内存溢出。

内存溢出:程序运行过程中,申请的内存大于系统能够提供的内存,导致程序无法申请到足够的内存。

# 内存泄漏

| 分类

内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用。由于长生命周期对象持有短生命周期对象的引用,短生命周期对象即使不再被需要也不能被 GC 回收。主要分为以下四类:

* 常用性内存泄漏

发生内存泄漏的代码会被多次执行使用,每次执行都会导致一块内存泄漏。

* 偶发性内存泄漏

发生内存泄漏的代码只有在某些特定环境或者操作过程下才会发生。

常发性和偶发性是相对的。在特定的环境中,偶发性会变成常发性,常发型也会变成偶发性。所以测试环境和测试方法对于检测内存泄漏至关重要。

* 一次性内存泄漏

发生内存泄漏的代码只会执行一次,由于算法的缺陷,导致有且仅有一块内存发生泄漏。

比如 C++ 在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

* 隐式内存泄漏

程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

| 常见的内存泄漏

* 静态集合类或数组引起的内存泄漏

HashMap、Vector 等集合类或数组的使用很容易出现内存泄漏。静态变量的生命周期和程序一致,当它们结束或修改时,它们引用的对象不能被释放而无法被 GC 回收。

在下面这个例子中,循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果仅仅使用 o=null 释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

  1. Vector<Object> v = new Vector<Object>(100);
  2. for (int i = 1; i<100; i++) {
  3. Object o = new Object();
  4. v.add(o);
  5. o = null;
  6. }

再比如在数组实现栈时,我们对 pop 的实现中,被弹出的元素的引用仍然存在与数组中,但是这个元素已经是永远不会被访问。这种情况(保存一个不需要的对象的引用)也被成为游离,GC 无法判断游离的情况。只需要将弹出的数组元素的值设为 null 即可,这样可以覆盖无用的引用并使系统可以在用例使用完被弹出的元素后回收内存。

  1. public Item pop() {
  2. // 从栈顶删除元素
  3. item item = a[--N];
  4. // 避免对象游离
  5. a[N] = null;
  6. if (N > 0 && N == a.length / 4) resize(a.length / 2);
  7. return item;
  8. }

* 修改 HashSet 中对象的参数值,且参数是计算哈希值的字段

当一个对象被存储到 HashSet 集合中以后,修改了这个对象中那些参与计算哈希值的字段后,这个对象的哈希值与最初存储在集合中的就不同了,这种情况下,用 contains() 方法在集合中检索对象是找不到的,这将会导致无法从 HashSet 中删除当前对象,造成内存泄漏:

  1. public static void main(String[] args){
  2. Set<Person> set = new HashSet<Person>();
  3. Person p1 = new Person("张三","1",25);
  4. Person p2 = new Person("李四","2",26);
  5. Person p3 = new Person("王五","3",27);
  6. set.add(p1);
  7. set.add(p2);
  8. set.add(p3);
  9. // 总共有:3 个元素!
  10. System.out.println("总共有:"+set.size()+" 个元素!");
  11. // 修改p3的年龄,此时p3元素对应的hashcode值发生改变
  12. p3.setAge(2);
  13. // 此时remove不掉,造成内存泄漏
  14. set.remove(p3);
  15. // 重新添加,可以添加成功
  16. set.add(p3);
  17. // set中的元素个数为4个
  18. System.out.println("总共有:"+set.size()+" 个元素!");
  19. for (Person person : set){
  20. System.out.println(person);
  21. }
  22. }

| 内存泄漏的解决方案

  1. 尽早释放无用对象的引用。
  2. 避免在循环中创建对象。
  3. 使用字符串处理时避免使用 String,应使用 StringBuffer。
  4. 尽量少使用静态变量,因为静态变量存放在永久代,基本不参与垃圾回收。

    # 内存溢出

    OOM:OutOfMemoryError

| 原因

发生内存溢出的主要原因分为以下几点:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
  2. 集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收(内存泄漏)。
  3. 代码中存在死循环或循环产生过多重复的对象实体。
  4. 启动参数内存值设定的过小。

    | 常见情况

    * 持久带溢出:java.lang.OutOfMemoryError: PermGen space

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

    * 堆溢出:java.lang.OutOfMemoryError: Java heap space

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

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

* 虚拟机栈和本地方法栈溢出

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

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

| 内存溢出的解决方案

  • 使用内存查看工具动态查看内存使用情况;

  • 修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx 参数一定不要忘记加)

  • 检查错误日志,查看 OutOfMemory 错误前是否有其它异常或错误。

  • 对代码进行走查和分析,找出可能发生内存溢出的位置。

    # 参考

  1. 内存泄露与内存溢出的区别
  2. 内存溢出与内存泄漏