JAVA内存泄漏和内存溢出

1 内存泄漏(memory leak)

内存泄漏是指你向系统申请分配内存进行使用(allocate),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

内存泄漏有两个前提,才能被称为内存泄漏:

  1. 这个对象或者这块内存,逻辑上已经不再使用了,或者说我们认为它本应该被清理回收掉。
  2. 但因为还有引用指向它,导致无法被gc回收。

如果同时这个对象或者内存比较大,那就是比较明显的内存泄漏的,影响会大很多。

一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出

1.1 内存泄漏的分类

以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

1.2 内存泄漏的原因

内存泄漏可能至少有如下这些原因:

  1. static关键字:
    • static关键字使一个变量变为只和这个类相关的类变量。他的生命周期很长,贯穿jvm的启动到关闭。如果在逻辑上该字段修饰的数据已经被弃用了,但因为它被static修饰,那就发生了内存泄漏,尤其是当static修饰的引用指向一个大对象的时候
  2. 内部类:
    • 内部类有个特性,是他会持有一个外部类的引用。如果内部类的实例一直存活,那么外部类的实例也就一直存在,此时如果外部类持有大对象,那么也就发生了内存泄漏。
  3. 第三方库:
    • 我们平时会用到很多第三方库,比如ButterKnife、EventBus、RxJava等等,在使用的时候,都有一个先registerd或者bind的操作,如果注册或者绑定的是一个大对象,并且结束时没有unregister或者unbind,就会容易造成内存泄漏。
  4. 集合类:
    • 我们平常使用的list/map等集合类,里面的元素如果不remove(),就会一直被集合类对象引用,而如果此时集合类对象如果又恰好是static的或者长期被引用的,那么集合类中的元素就会一直存在,即便我们早就不使用它们了。

1.3 内存泄漏的检测工具

  1. Compuware DevPartner Java Edition-包含Java内存检测,代码覆盖率测试,代码性能测试,线程死锁,分布式应用等几大功能模块。
  2. Quest JProbe-分析Java的内存泄漏。
  3. ej-technologies JProfiler-一个全功能的Java剖析工具,专用于分析J2SE和J2EE应用程序。它把CPU、执行绪和内存的剖析组合在一个强大的应用中。
  4. BEA JRockit-用来诊断Java内存泄漏并指出根本原因,专门针对Intel平台并得到优化,能在Intel硬件上获得最高的性能。

2 内存溢出(out of memory)

指程序申请内存时,没有足够的内存供申请者使用,或者说,你需要一块存储long类型的数据的存储空间,但是却给了你一块存储int类型数据的空间,那么结果就是内存不够用,此时就会报错OutOfMemoryError(简称OOM),即所谓的内存溢出。

严重的内存泄漏,或者内存泄漏的堆积,可能会造成OOM的发生,但我们需要注意的是:

  • 并非所有OOM都意味着由内存泄漏引起,它可能只是单纯的由于生成大量局部变量或其他此类事件导致。
  • 另一方面,并非所有内存泄漏都必然表现为OOM。

1.2 内存溢出的原因

引起内存溢出的原因有至少以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;超出了系统可以提供的存储空间。
  2. 内存泄漏的堆积;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 代码中存在不断的递归,大量的栈空间消耗了剩下的内存;
  5. 使用的第三方软件中的BUG;
  6. 启动参数内存值设定的过小;

注意,递归时如果报StackOverFlowError,是表示调用栈深度超过限制。如果报OOM,则是栈深度还为超限,但JVM试图去扩展栈空间的的时候失败。

1.3 内存溢出的类型

OOM是内存溢出的常见指示,当没有足够的空间来分配新对象时,会抛出错误。当垃圾收集器找不到必要的空间,并且堆不能进一步扩展,会多次尝试。因此,会出现错误以及堆栈跟踪。

诊断OOM的第一步是确定错误的实际含义。这听起来很清晰,但答案并不总是那么清晰。例如:OOM是否是因为Java堆已满而出现,还是因为本机堆已满?为了帮助您回答这个问题,让我们分析一些可能的错误消息:

  • java.lang.OutOfMemoryError: Java heap space
  • java.lang.OutOfMemoryError: PermGen space(jdk 1.8前)
  • java.lang.OutOfMemoryError: Metaspace
  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  • java.lang.OutOfMemoryError: request bytes for . Out of swap space?
  • java.lang.OutOfMemoryError: (Native method)
  • java.lang.OutOfMemoryError: Direct buffer memory
  • java.lang.OutOfMemoryError: Kill process or sacrifice child

1.3.1 Java heap space

当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。

堆内存溢出的原因可能有如下几点:

  1. 大的对象的申请
  2. 大文件上传
  3. 大批量从数据库中获取数据
  4. 从JDK8开始,String常量池放入了堆,如果不同的String数量太多,也会发生堆内存溢出。

首先,如果代码没有什么问题的情况下,可以适当调整-Xms-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。

其次,尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。

最后,尽量提高一次请求的执行速度,垃圾回收越早越好,否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。

1.3.2 PermGen space

当出现java.lang.OutOfMemoryError: PermGen space异常时,这里的PermGen space其实指的是方法区。

方法区是VM的规范,所有虚拟机必须遵守的。常见的JVM虚拟机Hotspot、JRockit(Oracle)、J9(IBM)都要实现自己的方法区。

PermGen space永久代则是HotSpot虚拟机基于JVM规范对方法区的一个落地实现,并且只有HotSpot才有PermGen space。不过在JDK8及以后,Metaspace元空间成为了HotSpot虚拟机对方法区的新的实现,替换了PermGen space

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

由于永久代是存储类和方法对象的区域,那么永久代OOM的原因可能是:

  1. 系统的代码非常多,要存储的类的相关信息太多,导致内存溢出。
  2. 引用的第三方包非常多,要存储的类的相关信息太多,导致内存溢出。
  3. 通过动态代码生成类加载等方法(如生成jsp页面),导致永久代的内存占用很大。
  4. 生成的String太多,导致字符串常量池膨胀。

如果代码没有什么问题的情况下,可以适当调整-MaxPermGenjvm参数。

值得注意的是,移除PermGen的工作从JDK7就开始了,永久代的部分数据在JDK7就已经转移到了Java Heap或者是Native Heap。

  • 字面量(interned strings),转移到Java heap;
  • 类的静态变量(class statics)转移到Java heap;
  • 符号引用(Symbols)转移到Native heap;
  • 运行时常量池,比如String常量池等,转移到Java heap;

所以如果是JDK7版本中报错OOM:PermGen space,那么至少可以排除上述几项数据发生溢出的可能。

1.3.3 Metaspace

当出现java.lang.OutOfMemoryError:Metaspace异常时,就是元空间溢出了。

前文我们说过,在JDK8及以后,Metaspace元空间成为了HotSpot虚拟机对方法区的新的实现,替换了PermGen space。元空间并不在虚拟机中,而是使用本地内存。

由于Metaspace元空间是存储类和方法对象的区域,那么元空间OOM的原因可能是:

  1. 系统的代码非常多,要存储的类的相关信息太多,导致内存溢出。
  2. 引用的第三方包非常多,要存储的类的相关信息太多,导致内存溢出。
  3. 通过动态代码生成类加载等方法(如生成jsp页面),导致元空间的内存占用很大。

我们可以优化参数配置来解决元空间溢出的问题,默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,以免造成整机的服务停机。

  • -XX:MetaspaceSize,设置初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

  • -XX:MaxMetaspaceSize,设置最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与GC相关的属性:

  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

除此之外,慎重引用第三方包,以及关注动态生成类的框架,也是有效的解决方法。

1.3.3 Requested array size exceeds VM limit

有的时候会碰到这种内存溢出的描述Requested array size exceeds VM limit,一般来说java对应用程序所能分配数组最大大小是有限制的,只不过不同的平台限制有所不同,但通常在1到21亿个元素之间。

Requested array size exceeds VM limit错误出现时,意味着应用程序试图分配大于Java虚拟机可以支持的数组。JVM在为数组分配内存之前,会执行特定平台的检查:分配的数据结构是否在此平台是可寻址的。

因此数组长度要在平台允许的长度范围之内。不过这个错误一般少见的,主要是由于Java数组的索引是int类型。Java中的最大正整数为2 ^ 31 - 1 = 2,147,483,647,而平台特定的限制非常接近这个数字。

1.3.4 Request bytes for . Out of swap space

此消息似乎是一个OOM,当本地堆的分配失败并且本地堆可能将被耗尽时,HotSpot VM会抛出此异常。消息中包括失败请求的大小(以字节为单位)以及内存请求的原因。在大多数情况下,是报告分配失败的源模块的名称。

如果抛出此类型的OOM,则可能需要在操作系统上使用故障排除实用程序来进一步诊断问题。在某些情况下,问题甚至可能与应用程序无关。例如,您可能会在以下情况下看到此错误:

  • 操作系统配置的交换空间不足。
  • 系统上的另一个进程消耗了所有可用的内存资源。
  • 由于本机泄漏,应用程序也可能失败(例如,如果某些应用程序或库代码不断分配内存但无法将其释放到操作系统)。

1.3.5 Native method

java.lang.OutOfMemoryError: (Native method)

如果您看到此错误消息并且堆栈跟踪的顶部框架是本地方法,则该本地方法遇到分配失败。此消息与上一个消息之间的区别在于,在JNI或本地方法中检测到Java内存分配失败,而不是在Java VM代码中检测到。

如果抛出此类型的OOM,您可能需要在操作系统上使用实用程序来进一步诊断问题。

1.3.6 Direct buffer memory

在使用ByteBuffer中的allocateDirect()这类申请直接内存空间的时候可能会遇到,这类申请直接内存空间的方法在java NIO(像netty)等框架中被经常使用,出现该问题时会抛出java.lang.OutOfMemoryError: Direct buffer memory异常。

如果你在直接或间接使用了ByteBuffer中的allocateDirect方法,却没有clear的时候就会出现类似的问题。

如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize,并及时clear内存。

1.3.7 Kill process or sacrifice child

在描述该问题之前,先熟悉一点操作系统的知识:操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,称为内存杀手(Out of memory killer)。当内核检测到系统内存不足时,OOM killer被激活,检查当前谁占用内存最多然后将该进程杀掉。

一般Out of memory:Kill process or sacrifice child报错会出现在可用虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时。在这种情况下,OOM Killer会选择流氓进程并杀死它。

0%