记一次解决OOM问题的过程

1. 什么是OOM

OOM-Out Of Memory,内存溢出,来源于'java.lang.OutOfMemoryError'。Oracle官方解释为,"Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. OutOfMemoryError objects may be constructed by the virtual machine as if suppression were disabled and/or the stack trace was not writable.",意为当Java虚拟机由于内存不足而无法分配对象,并且垃圾回收器无法再提供更多内存时,抛出该异常。

2. OOM出现的原因

OOM的原因主要有两点:

1)分配过少:应用启动时通过VM参数分配的内存过少。

2)应用耗内存过多且使用后未释放:这种情况下会造成内存泄露或者内存溢出。

内存泄露:申请使用的内存未被释放,虚拟机无法分配这部分内存。

内存溢出:申请的内存超出了JVM能提供的内存大小,成为溢出。

Java语言存在垃圾自动回收机制,开发者不需要自行申请与释放内存。理论上不会存在“内存泄露”的情况,但如果存在不规范的编码,例如将某个大对象的引用放入一个全局的Map中,虽然方法被回收了,但是由于JVM通过对象的引用情况拉了进行内存回收,,导致大对象不能及时被回收,就会导致内存溢出。

3. OOM的类型

JVM内存模型:

1)程序计数器:一块较小的内存空间,它可以看作是当前线程所执行的字节码的型号指示器。

2)Java虚拟机栈:线程私有,与线程的生命周期相同。方法执行的内存模型,每一个方法执行的同时都会创建一个栈帧(Stack frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

3)本地方法栈:类似Java虚拟机栈,但是是为本地Native方法提供的。

4)Java堆:所有线程共享的一块内存区域,唯一目的就是存放内存实例,垃圾回收的主要区域。

5)运行时常量池:方法去的一部分,存储常量信息,如字面量、符号引用而已。

6)直接内存:并不是JVM运行时数据区的一部分,可以直接访问的内存。

最常见的OOM情况有以下三种:

1)  java.lang.OutOfMemoryError : Java heap space -----> java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起的。对于内存泄露或者对的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数 -Xms,-Xmx 等修改。

2) java.lang.OutOfMemoryError: PermGen space ------> java 永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp陀,或者采用cglib等反射机制的情况,因为上述情况会产生大量的class信息存储于方法区。此种情况可以更改方法区的大小来解决,使用类似 -XX:PermSize=64m  -XX:MaxPermSize=256m的形式修改。另外,过对的常量尤其是字符串会造成方法区溢出。

3) java.lang.StackOverfloeError ----> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于在程序中死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过设置虚拟机参数 -Xss 来设置栈的大小。

4. 解决过程

1)猜测阶段

开始是在测试接口的时候发现接口请求超时,观察到连接虚拟机的Xshell上一直在打印 java.lang.OutOfMemoryError和定时任务失败的堆栈信息。由于没有什么经验,便武断地觉得是堆内存大小不足导致的,将 -Xmx (最大堆大小增加了一倍)并重启。但应用在运行一段时间后,同样打印上述的错误堆栈并请求超时。

2)分析阶段

重启应用,并让应用运行一段时间, 先通过ps命令查看应用的pid,再通过命令

jstat -gcutil pid 1000 10

每隔一秒钟打印出应用内存的使用情况以及gc的情况。发现老年代占用率在90%以上,full gc的次数越来越频繁,猜测是由于代码不规范造成的内存溢出。

接着用

top -H  -p pid

来找到cpu使用率比较高的一些线程,然后用下面的命令将使用最高的pid转换为16进制得到nid

printf '%x\n' pid

然后直接使在jstack中找到对应的堆栈信息

jstack pid | grep 'nid' -CS -color

发现CPU占用最高的一个是VM thread,一个是新增的定时任务。VM thread是jvm进行gc的线程,说明应用进行gc的频率过高,并且猜测与这一新增的定时任务有关。

3)验证修复阶段

通过对应用的堆快照进行分析可以确定造成内存泄漏的具体类是什么。堆快照的生成可以有两种方式。

1.jvm参数设置

(添加环境变量) Java -jar

-XX:+HeapDumpOnOutOfMemoryError (表明进行统计相关heapDump文件再OOM的时候)

-XX:HeapDumpPath=/export/Domains/rcsv-fm.wd.local/server1/logs/gc.hprof(表明会导出生产的HeapDump文件的路径)

2.使用jvisualvm工具进行即时观察

在应用的启动参数中添加如下参数

-Djava.rmi.server.hostname=虚拟机ip

-Dcom.sun.management.jmxremote=true

-Dcom.sun.management.jmxremote.port=33306 

-Dcom.sun.management.jmxremote.authenticate=false

-Dcom.sun.management.jmxremote.ssl=false

本地打开jdk目录下的bin/jvisualvm程序,在远程的目录下添加远程主机和JMX连接,便可即时观察应用的内存使用状况、cpu使用状况以及各个类占用堆内存的状况。


cpu及内存占用率
各个类的内存占用率

通过堆快照可以看出,char[]、String、InvolvorsBean三种类型的内存占用率是最高的,对代码进行优化。

你可能感兴趣的:(记一次解决OOM问题的过程)