基本概念

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

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

内存泄漏

分类

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

常用性内存泄漏

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

偶发性内存泄漏

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

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

一次性内存泄漏

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

隐式内存泄漏

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

常见的内存泄漏

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

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

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

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

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

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

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

当一个对象被存储到HashSet中,如果修改了这个对象中参与计算哈希值的字段,那么这个对象的哈希值就与最初存储在集合中的不同。这种情况下,使用contains()方法无法在集合中检索到该对象,也就无法从HashSet中删除当前对象,造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
// 总共有:3 个元素!
System.out.println("总共有:"+set.size()+" 个元素!");
// 修改 p3 的年龄,此时 p3 元素对应的 hashcode 值发生改变
p3.setAge(2);
// 此时 remove 不掉,造成内存泄漏
set.remove(p3);
// 重新添加,可以添加成功
set.add(p3);
// set 中的元素个数为 4 个
System.out.println("总共有:"+set.size()+" 个元素!");

for (Person person : set){
System.out.println(person);
}
}

内存泄漏的解决方案

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

内存溢出

内存溢出,即 OOM(OutOfMemoryError),主要分为堆内存溢出和栈异常。

主要发生原因

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

堆内存溢出

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

原因为堆内存不不够,例如创建的对象太多,在进行垃圾回收之前对象数量达到了最大堆的容量限制。

解决方法:

  • 通过工具查看泄漏对象到 GC Roots 的引用链,定位出泄漏代码的位置,检查是否有死循环不断创建对象。

  • 如果是因为堆内存不够,应该检查虚拟机的堆参数 -Xmx (最大堆大小)和 -Xms (初始堆大小),与机器物理内存对比看是否可以调大。

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

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

解决方法:调整 JVM 参数中的 -XX 和 -XXermSize。

1
2
-XX:MaxPermSize=128m
-XXermSize=128m

GC 开销溢出:java.lang.OutOfMemoryError: GC overhead limit exceeded

JDK1.6 新增的错误类型,当 GC 为释放很小的空间占用大量时间时抛出。例如 JVM 消耗了 98%的时间进行垃圾回收去释放很小的可用内存。一般是因为堆内存很小,导致没有足够的内存。

解决方法:

  • 查看系统是否有使用大内存的代码或死循环。
  • 通过添加 -XX:-UseGCOverheadLimit 的 JVM 配置参数来限制使用内存。

栈异常

虚拟机栈溢出:java.lang.OutOfMemoryError: unable to create new native thread

虚拟机的栈空间不足够创建新的线程或者是扩展栈时无法申请到足够的空间时抛出该错误。

系统的用户空间通常一共是 3G,除了 Text/Data/BSS /MemoryMapping 几个段之外,Heap 和 Stack 空间的总量是有限的。因此遇到这个错误,可以通过两个途径解决:

  • 通过-Xss 启动参数减少单个线程栈大小,这样便能开更多线程。但是单个线程的栈内存也不能太小,不然会抛出StackOverflowError
  • 通过-Xms 和-Xmx 两参数减少 Heap 大小,将内存让给 Stack(前提是保证 Heap 空间够用)。

线程栈溢出:java.lang.StackOverflowError

线程请求的栈深度大于虚拟机所允许的最大深度,例如递归被无限调用时就会抛出这种错误。因为函数的调用在内存中会开辟新的空间存放子函数,递归函数更是会不断占用栈空间。当递归过深时,栈空间就会不断被耗尽导致StackOverflowError。此外,线程栈确实很小时也会抛出这个错误。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
// 抛出StatckOverflowError
System.out.println(factorial(1000000000));
}

public static int factorial (int n) {
if (n == 1) {
return 1;
}
return factorial(n - 1) * n;
}

解决方法:减少方法的调用层次或者调整 -Xss 参数增加线程栈大小,使用递归时要尤其注意这个错误。

内存溢出的常见解决方案

  • 使用内存查看工具动态查看内存使用情况。
  • 对代码进行走查和分析,找出可能发生内存溢出的位置
  • 修改 JVM 启动参数,直接增加内存(-Xms,-Xmx 参数一定不要忘记加)。
  • 检查错误日志,查看 OutOfMemory 错误前是否有其它异常或错误。

参考

  1. 内存泄露与内存溢出的区别
  2. 内存溢出与内存泄漏
  3. JVM栈中可能出现的异常以及如何设置栈的大小
  4. JVM中的堆栈溢出
  5. 什么是 Stack Overflow,什么情况下会造成 Stack Overflow
  6. GC overhead limit exceeded原因分析及解决方案