常见OOM现象

《java 特种兵 上册》 3.6 常见的OOM现象( 136-146页),看此节后的总结。

OOM的实际场景是很多的,这里介绍常见的,同时结合网络与实际测试中的一些资料信息。

 

一.HeapSize OOM(堆空间内存溢出)

关键字:java.lang.OutOfMemoryError:java heap space

意思:堆空间溢出。老年代区域剩余的内存,已经无法满足将要晋升到老年代区域的对象大小,会报此错。

 一般来说,绝大部分都是这种情况。大量空间占据了堆空间,而这些对象持有强引用,导致无法回收,当对象大小之和大于由xmx参数指定的堆空间大小时,溢出错误就发生了。

发生堆内存不足的原因可能有:

1)设置的堆内存太小,而系统运行需要的内存要超过这个设置值

2)内存泄露。关注系统稳定运行期,full gc每次gc后的可用内存值是否一直在增大。

3)由于设计原因导致系统需要过多的内存,如系统中过多地缓存了数据库中的数据,这属于设计问题,需要通过设计减少内存的使用。

4)分析线程执行模型和它们持有的JVM里的短生命对象

 

代码层面处理方式和手段:

1)看代码是否有问题;.eg:List.add(" ")在一个死循环中不断的调用add却没有remove。---间接内存泄露

2)代码没有问题,并发导致。

并发导致内存还不能被gc掉,或很多对象还会引用,那就说明对象所在的生命周期范围内的代码部分还没执行结束。

解决方法有:

1.代码提速。这样可以使得相同对象的生存时间更短。更快被GC。

2.对于长生命周期对象(如I/O操作)对象后续不用了,objecft=null可以辅助GC,一旦方法脱离了作用域,相应的局部变量应用就会被注销。

3.代码无法优化后,程序跑的飞快,还是出现OOM,考虑到去修改参数配置

 eg:堆空间的大小,堆空间要设置的足够大(相对),如果太大,发生FULL GC会很恐怖

4.内存泄露:内存可能在某些情况增加几十字节空间但未能释放,每次被GC,很老的对象被GC的较慢。

eg1:Session中放数据,Session回话消失才会被注销,但是会话要很长的时间才会被注销。

eg2.不断full gc,每次时间变长频率变小。当次数达到一定量时,平均full gc时间达到一定比例时会报:OutOfMemoryError:GC over head limit exceeded

不断的FULLGC就是不抛出OOM的现象,出现这种现象通常是一些藏匿的Bug或配置导致。

eg3:Tomcat的session导致,session信息保存在全局的currentHashMap中,大量的HTTPClient访问创建的临时session。但并没有保存系统中为之分配的SessionKey中和相关的Cookies信息,导致每次请求都创建session(几十个字节,request.getSession被调用时),一般看不出来,是一个堆积如山的过程。

多种OOM,大多是指系统一个静态引用指向了一个只增不减的对象(例如 hashMap)

 

GC效率低下引起的OOM

如果堆空间小,那GC所占时间就多,回收所释放的内存不会少。根据GC占用的系统时间,以及释放内存的大小,虚机会评估GC的效率,一旦虚机认为GC的效率过低,就有可能直接抛出OOM异常。但这个判定不会太随意。

Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收 了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。“

一般虚机会检查几项:

  ● 花在GC上的时间是否超过了98%

  ● 老年代释放的内存是否小于2%

  ● eden区释放的内存是否小于2%

  ● 是否连续最近5次GC都出现了上述几种情况(注意是同时出现)

只有满足所有条件,虚机才会抛出OOM:GC overhead limit exceeded

这个只是辅助作用,帮助提示系统分配的堆可能大小,并不强制开启。有关闭开关-XX:-UseGCOverheadLimit来禁止这种。

此参数的意义:当频繁Full GC导致程序僵死现象,一致耗着,如果加上-XX:+UseGCOverheadLimit参数就可以让程序提前退出,避免僵死程序长期占用资源。

 

遇到此种情况如何解决?

解决这种问题两种方法:

1)增加参数,- XX:-UseGCOverheadLimit,关闭这个特性,同时增加heap大小,-Xmx1024m。

2)排查并优化消耗内存资源代码。

如果生产环境中遇到了这个问题,在不知道原因时可以通过-verbose:gc -XX:+PrintGCDetails看下到底什么原因造成了异常。通常原因都是因为old区占用过多导致频繁Full GC,最终导致GC overhead limit exceed。如果gc log不够可以借助于JProfile等工具查看内存的占用,old区是否有内存泄露。分析内存泄露还有一个方法-XX:+HeapDumpOnOutOfMemoryError,这样OOM时会自动做Heap Dump,可以拿MAT来排查了。还要留意young区,如果有过多短暂对象分配,可能也会抛这个异常。

日志的信息不难理解,就是每次gc时打条日志,记录GC的类型,前后大小和时间。举个例子。

33.125: [GC [DefNew: 16000K->16000K(16192K), 0.0000574 secs][Tenured: 2973K->2704K(16384K), 0.1012650 secs] 18973K->2704K(32576K), 0.1015066 secs]

100.667:[Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] 

GC和Full GC代表gc的停顿类型,Full GC代表stop-the-world。箭头两边是gc前后的区空间大小,分别是young区、tenured区和perm区,括号里是该区的总大小。冒号前面是gc发生的时间,单位是秒,从jvm启动开始计算。DefNew代表Serial收集器,为Default New Generation的缩写,类似的还有PSYoungGen,代表Parallel Scavenge收集器。这样可以通过分析日志找到导致GC overhead limit exceeded的原因,通过调节相应的参数解决问题。

 

案例一原文

现象:某应用系统负载升高,响应变慢,发现应用进行频繁GC,甚至出现OutOfMemroyError: GC overhead limt exceed的错误日志。 

原因:因为这个项目是个历史项目,使用了Hibernate ORM框架,在Hibernate中开启了二级缓存,使用了Ehcache;但是在Ehcache中没有控制缓存对象的个数,缓存对象增多,导致内存紧张,所以进行了频繁的GC操作。 

总结:使用本地缓存(如Ehcache、OSCache、应用内存)时,一定要严格控制缓存对象的个数及声明周期。

 

案例二原文

现象:某应用系统改变用户web接口(使用JSON作为数据负载)后,性能下降,出现了“OutOfMemoryError: Java heap space”问题并伴有过多的垃圾收集。通过verbose:gc 日志分析确认32-bit HotSpot JVM 老年代空间(1 GB 容量)被完全消耗。

分析过程:

1)线程转储数据分析,这种数据通常会告诉在JVM堆里面内存分配的罪魁祸首线程。同样的它也会显示任何一个尝试从远程系统发送和接受数据的贪婪或者阻塞的线程。观察到的OOM事件和阻塞线程之间有很近的关联关系。线程出现了阻塞并且花费了很长时间从远程系统里读和接收JSON响应。

2)堆转储分析,找出堵塞线程和JVM堆转储分析之间的关联,并且确定这个阻塞线程从堆里占用了多少内存。

--》MAT工具打开文件

-》直方图查看前通过过ExecuteThread(用于对象的创建和执行)过滤。总共有多少个线程被创建,这些线程总共持有的内存占用是多少。--判断问题核心是不是源于线程自己的内存占有。

--》深入分析线程内存持有。右键点击ExecuteThread 类并且选择“列出所有外部引用的对象”。--发现“STUCK(阻塞)”线程和大量内存占用有很大关系。

---》线程局部变量鉴别。展开几个线程示例并了解内存占用的原始来源。---发现根源在于大量的JSON数据响应。

总结:发现少量的线程花费太多时间去读取和接收JSON响应,这是大量数据负载的一个明显的症状。通过方法局部变量创建的短生命对象也会出现在堆转储分析中。然而,其中的一些仅仅能被他们的父线程看到,这是由于他们没有被其他对象引用,比如这个例子。为了找出真正的调用者你或许也需要分析这个线程栈,随后通过代码审查确定最终的根源。在某些情形下,大量的JSON数据可以达到45M以上。使用了32位JVM而且仅仅只有1G的老年代,结果只需要几个线程就足够触发一些性能下降。

 

案例三:jsp页面导致tomcat内存溢出

背景:测试kafka队列时,消费者使用一个jsp页面模拟,使用tomcat中间件。

现象:测试一段时间后,发现JVM内存持续增加,涨到一定值后就一直频繁做gc且JVM内存并没有释放多少,同时频繁gc引起CPU使用率变高。dump堆内存,分析查看主要是session占用。

分析:发现每请求一次jsp页面,会产生一个session对象,并且这个对象30分钟(web.xml里设置的)后才过期。

解决方法:

1.在page指令里添加session=“false”。设置jsp页面session参数  <%@ page session="false" language="java" pageEncoding="UTF-8" %>

2.在web.xml里把session的过期时间设成0。

参考:JSP页面导致tomcat内存溢出一例session不及时释放导致内存溢出的性能问题分析

 

 

 

二.PermGen OOM(永久代内存溢出)

关键字:java.lang.OutOfMemoryError:PermGen space

永久代(PermGen space)是JVM实现方法区的地方,因此该异常主要设计到方法区和方法区中的常量池。永久代存放的东西有class和一些常量。perm是放永久区的。如果一个系统定义了太多的类型,那永久区可能会溢出。jdk1.8中,被称为元数据区。

A.常量池(JDK1.6,JDK1.7以后常量池不会放在永久代中了。)

string常量对象会在常量池(包含类名,方法名,属性名等信息)中以hash方式存储和访问,hash表默认的大小为1009,当string过多时,可以通过修改-xx:stringtableSize参数来增加Hash元素的个数,减少Hash冲突。

当常量池需要的空间大于常量池的实际空间时,也会抛出OutOfMemoryError: PermGen space异常。

例如,Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么可以通过String.intern方法来模拟一下运行时常量区的溢出.

 

B.class加载

由于class被卸载的条件十分的苛刻,这个class所对应的classLoader下面所有的class都没有活对象的应用才会被卸载。

方法区(Method Area)不仅包含常量池,而且还保存了所有已加载类的元信息。当加载的类过多,方法区放不下所有已加载的元信息时,就会抛出OutOfMemoryError: PermGen space异常。主要有以下场景: 

  • 使用一些应用服务器的热部署的时候,会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。

  • 如果应用程序本身比较大,涉及的类库比较多,但分给永久代的内存(-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。

解决方法:在每次CGlib动态创建时,都重新给它设置一个classLoader,这样在运行代码就不会出现OOM,会发现大量的class被卸载。

VisualVm工具,查看PermGen标签页、类加载标签页中的趋势。随着类装载的数量增加,最终会出现了java.lang.OutOfMemoryError: PermGen space。

 

示例:如果不断产生新类,而没有回收,那最终很可能会导致永久区溢出。
解决的话从几方面入手:
● 增加MaxPermSize
● 减少系统需要的类数量
● 使用classloader合理的装载各个类,并定期进行回收

 

加+PrintGCDetails参数,打印日志可看地gc情况。+TraceClassUnloading,查看日志。  

 

 

什么是Java的永久代(PermGen)内存泄漏 

在反射的过程中,会有一些新的类被动态创建出来,如果系统中频繁地有新的类被动态创建出来,并且将禁止了class的GC,此时很容易导致永久内存区溢出。

增加-verbose:class很容易观察到class输出。

最好不要加-noclassgc选项(运行期间就不会做class的GC),加上这个选项后,势必需要更大的永久内存,很容易造成永久内存区溢出。

java.lang.OutOfMemoryError: Metaspace

JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但永久代仍存在于JDK1.7中,并没完全移除。 
JDK 8.HotSpot JVM使用本地化的内存存放类的元数据,这个空间叫做元空间(Metaspace)。官方定义:”In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace”。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:-XX:MetaspaceSize、-XX:MaxMetaspaceSize。 

VisualVm工具,查看Metaspace标签页、类加载标签页中的趋势,可以看到使用的空间大小随类装入的数量增加而增加,这也说明了Metaspace是用来存放类的元数据的。 

 

三.DirectBuffer OOM(直接内存内存溢出)

关键字:OutOfMemoryError: Direct buffer memory 

Java中普通I/O用输入/输出流方式实现,输入流InputStream(终端—>直接内存->JVM),输出流(JVM->直接内存->终端),这一过程中有kenel与JVM之间的拷贝(很多次),为了使用直接内存,Java是有一块区域叫DirectBuffer,不是JavaHeap而是cHeap的一部分。

NIO支持直接内存的使用,也就是通过java代码,获得一块堆外的内存空间,这块空间是直接向操作系统申请的。它的申请速度比堆内存慢,但访问速度快。
对于那些可复用的,并会被经常访问的空间,使用直接内存可提高系统性能。但由于直接内存没有被java虚机完全托管,若使用不当,也容易触发溢出,导致宕机。

ByteBuffer有两种一种是heap ByteBuffer,该类对象分配在JVM的堆内存里面,直接由Java虚拟机负责垃圾回收,一种是direct ByteBuffer是通过jni在虚拟机外内存中分配的。通过jmap无法查看该快内存的使用情况。只能通过top来看它的内存使用情况。JVM堆内存大小可以通过-Xmx来设置,同样的direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),在程序中中可以获得-XX:MaxDirectMemorySize的设置的值。

direct ByteBuffer通过full gc来回收内存的,direct ByteBuffer会自己检测情况而调用system.gc(),但是如果参数中使用了DisableExplicitGC那么就无法回收该快内存了,-XX:+DisableExplicitGC标志自动将System.gc()调用转换成一个空操作,就是应用中调用System.gc()会变成一个空操作。那么如果设置了就需要手动来回收内存。那么除了FULL GC还有别的能回收direct ByteBuffer吗?CMS GC会回收Direct ByteBuffer的内存。

直接内存不一定触发full gc(除非直接内存使用量达到了-XX:MaxDirectMemorySize的设置),保证它不溢出的方法是合理进行full gc(full gc时会对这里做gc)的执行,或设定一个系统实际可达的-XX:MaxDirectMemorySize值(默认是-xmx的设置)。

如果系统的堆内存少有GC发生,而直接内存申请频繁,会比较容易导致直接内存溢出。(32位机器比较明显)
为避免直接内存溢出,

1)合理执行显示GC,可降低概率;

2)设置合理的-XX:MaxDirectMemorySize值,避免发生;

3)设置一个较小的堆在32 虚机上可是到更多的内存用于直接内存。

手动释放本地内存?DirectByteBuffer持有一个Cleaner对象,该对象有一个clean()方法可用于释放本地内存,需要的时候可以调用这个方法手动释放本地内存。

如果应用里有任何地方用了direct memory,那么使用-XX:+DisableExplicitGC要小心。如果用了该参数而且遇到direct memory的OOM,可以尝试去掉该参数看是否能避开这种OOM。如果担心System.gc()调用造成full GC频繁,可以尝试 -XX:+ExplicitGCInvokesConcurrent 参数 ([HotSpot VM] JVM调优的"标准参数"的各种陷阱)。

 

eg:设置-xx:MaxDirectMemorysize:256    分配一个ByteBufferallocateDirect(257*1024*1024)就会导致OOM。

 

本地内存泄露?

分析gc信息,不存在Java堆内存的泄漏,但分析java进程,发现Java进程的总内存越来越大,并且无停止上涨的迹象,直到整个系统崩溃。它的定位比较复杂。

可能原因有:

1)如果系统存在JNI调用,本地内存泄露可能存在于JVM代码中。结合pmap等命令初步确认。

2)JDK的bug。如果第一步没问题,通过更新jdk版本,排除下。

3)操作系统bug 

本地内存泄漏可能还会引发异常:java.lang.OutOfMemoryError: unable to create new native thread

 

四.StackOverflowError(栈内存溢出错误)

关键字:StackOverflowError

栈(JVM Stack)存放主要是栈帧( 局部变量表, 操作数栈 , 动态链接 , 方法出口信息 )的地方。注意区分栈和栈帧:栈里包含栈帧。

与线程栈相关的内存异常有两个:

  • StackOverflowError(方法调用层次太深,内存不够新建栈帧)

  • OutOfMemoryError(线程太多,内存不够新建线程)

1.通常都是程序的问题,JVM对栈帧的大小设置已经很大了。

2.程序运行过程中,方法分派时,会分配frame来存放本地变量,栈,pc寄存器等信息,方法再调用方法会导致Java栈空间无止境的增长(死递归),Java的解决方法是:设置一个私有栈(不在堆内存,而是在NativeMemory),这个栈的空间大小,通过-Xss来设置,数量级在256K-1MB。如果使用空间超过了-Xss限制,请求新建栈帧时,栈所剩空间小于栈帧所需空间,就会出现StackOverflowError。

3.eg:死递归

死递归和死循环的区别:死循环类似于while(true)的操作,它的线程栈空间使用不会递增。而死递归需要记录退回的路径,递归过程中调用方法,每个方法运行过程中的本地变量。也就是要记录上下文信息。这些信息会随着内容的增加,占用很大的内存空间。

死递归:

eg:1.组件的复用。

     2.子类调用父类(复用父类的方法),父类调用子类(达到多态的效果),这中间要经过许多方法,可能形成环,进而形成死递归。

     3.三方框架的问题。

这种一般都是程序有问题。分析它时,输出的线程栈信息明确地说明了调用路径。

 

 

五.其他内存溢出

A.过多线程导致OOM:unable to creat new native thread

因为虚拟机会提供一些参数来保证堆以及方法区的分配,剩下的内存基本都由栈来占有,而且每个线程都有自己独立的栈空间(堆,方法区为线程共有)。所以:

  • 如果把虚拟机参数Xss调大了,每个线程的占用的栈空间也就变大了,那么可以建立的线程数量必然减少

  • 公式:线程栈总可用内存=JVM总内存-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存

如果-Xmx或者-XX:MaxPermSize太大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。

如果JVM正在请求操作系统创建一个本地线程,而操作系统无法创建的时候,就会出现这个报错信息。JVM中可以生成的最大线程数量由JVM的堆内存大小、Thread的Stack内存大小、系统最大可创建的线程数量(Java线程的实现是基于底层系统的线程机制来实现的,Windows下_beginthreadex,Linux下pthread_create)三个方面影响。Linux下JVM中可生成的最大Thread数量

简单理解为一个jvm进程的最大线程数为:虚拟内存/(堆栈大小*1024*1024),也就是说虚拟内存越大或堆栈越小,能创建的线程越多。

 

原因:物理内存不够用或者OS限制了单个进程使用的最大内存,大量的线程分配时,就有可能导致占用的Native Memory(线程私有栈)很多。 

每个线程开启都会占用系统内存,线程量太大时,也会导致OOm 。线程的栈空间也是在堆外分配的,和直接内存类似。如果想让系统支持更多的线程,那应使用一个较小的堆空间。

示例1,想要创建 1500个线程,但1200多失败了。提示“unable to create new native thread",表示系统创建线程数量已经饱和,其原因是java进程已经达到了可使用的内存上限。

示例2,死循环中创建线程并启动,线程内部申请变量,本身不退出。而且设置Os对进程内存的限制。

 

分析思路:

1)系统当前有过多的线程,操作系统无法再创建更多的线程。可以通过打印线程堆栈,先查看总的线程数量。可能原因:是否系统创建的线程被阻塞或者死锁,导致系统中的线程越来越多,直到超过最大限制。或者操作系统自身能创建的线程数量太少。

2)swap分区不足。

3)堆内存设置过大。

4)系统对进程内存限制,用户最大打开的进程数限制

max user processes用户最大打开的进程数这个值默认是1024,看官方说明,指用户最多可创建线程数,因为一个进程最少有一个线程,所以间接影响到最大进程数。

 

解决从以下几方面下手:

1)减少堆空间  -xmx,可预留更多内存用于线程创建,因此程序可正常执行。

2)减少每一个线程所占用的内存空间,-xss参数指定线程的栈空间     -xmx1g -xss128K    栈空间小了,栈溢出风险会提高。

3)打开/etc/security/limits.d/90-nproc.conf,把对应用户soft    nproc     1024这行的1024改大就行了。

总体思路:合理减少线程总数,减少最大堆空间,减少线程的栈空间也可行。 系统限制。

 

 

B.request{} byte for {}out of swap

地址空间不够用(不一定是物理地址,还有swap,显卡,网卡)

这个错误是当虚拟机向本地操作系统申请内存失败时抛出的。这和用完了堆或者持久化中的内存的情况有些不同。这个错误通常是在程序已经逼近平台限制的时候产生的。这个信息是可能已经用光了物理内存以及虚拟内存了。由于虚拟内存通常是用磁盘作为交换分区,因此最先想到的解决方法可能是先增加交换分区的大小,但这个方法可能不太好用。

 

C.IoException:too many open files

这个与OOM无关,但也是系统级别问题。它发生这类问题代表系统的某些设计存在问题或某些使用已经达到了权限。

错误提示:打开太多的文件,也可能是本地的socket打开太多,而没有被关闭,也就是说有太多没有关闭套接字的导致。

 

遇到OOM时,先大致分析原因,加上内存溢出后dump内存参数,设定参数后,出OOM会dump文件。用MAT或其他工作来分析 。

 

D、java.lang.OutOfMemoryError: Requested array size exceeds VM limit

 当准备创建一个超过虚拟机允许的大小的数组时,这条错误就会出现在眼前。64位的操作系统上,JDK7,如果数组的长度是Integer.MAX_VALUE-1,就会出现。

 

其他可参考资料:

写代码实现堆溢出、栈溢出、永久代溢出、直接内存溢出

posted @ 2017-08-04 14:05  milkty  阅读(13666)  评论(0编辑  收藏  举报