内存溢出异常OOM和内存泄露

578 阅读4分钟

「深入理解Java虚拟机」第2.4节读书笔记。

2.4 内存溢出异常OOM(OutOfMemoryError)

除了程序计数器外,虚拟机内存的其它几个运行时区域都有可能发生OOM。

2.4.1 Java堆溢出:Java heap space

  1. 产生:不断地创建对象,且GC Roots到对象之间可达,对象数量达到最大堆的容量限制。

  2. 处理:内存映像分析工具,确认内存中的对象是否是必要的,即区分是内存泄漏 or 内存溢出。

    • 内存泄漏:该对象不应该活着了。查看对象到GC Roots的引用链,哪里关联,导致垃圾收集器无法自动回收它们。

    • 内存溢出:内存中的对象确实还必须活着。

      • 检查虚拟机堆参数
      • 检查是否某些对象生命周期过长

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

  1. 产生
  • 线程请求的栈深度大于虚拟机允许的最大深度 StackOverflowError
  • 虚拟机在扩展栈时无法申请到足够的内存空间 OutOfMemoryError 已使用的栈空间太大 or 内存太小,本质都是栈空间无法继续分配。
  1. 处理
  • 阅读错误堆栈
  • 建立过多线程导致的内存溢出:减少最大堆+减少栈容量 换取更多的线程

2.4.3 方法区和运行时常量池溢出

  1. 产生:经常动态生成大量类

2.4.4 本机直接内存溢出

内存泄漏

内存泄露:不再使用的对象持续占有内存或占有的内存得不到及时释放,从而造成的内存空间的浪费,严重时会提示OutOfMemoryError。

内存溢出:程序运行过程中无法申请到足够的内存而导致的一种错误。

(内存泄露是内存溢出的一种诱因,不是唯一因素。)

根本原因:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

1、静态集合类引起内存泄露

HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象也不能被释放,因为他们也将一直被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 来说是不可回收的。

2、集合里的对象属性被修改后,再调用remove()方法时不起作用。(存疑!)

public static void main(String[] args)
{
    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);
    }
}

3、监听器

我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、各种连接

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

5、内部类和外部模块等的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:

public void registerMsg(Object b);

这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B是否提供相应的操作去除引用。

6、单例模式

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

7、ThreadLocal类

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。