什么是OOM?为什么会出现OOM?
概念
-
OOM,全称“Out Of Memory”,意思是“内存用完了” -
来源于
java.lang.OutOfMemoryError -
这是个特别严重的问题,因为这个问题已经 严重到应用程序自己无法处理了。
原因
官方的文档称,当
JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出java.lang.OutOfMemoryError: ···
具体原因一般有这两个:
- 自身原因:
分配的内存少了,比如JVM虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m -XX:HeapDumpPath=D:\oomTemp- ``-XX:+HeapDumpOnOutOfMemoryError`:表示 导出内存溢出的堆信息(hprof文件)
-Xms:表示 设定程序启动时占用内存大小-Xmx:表示 程序运行期间最大可占用的内存大小-XX:HeapDumpPath=:表示 生成得快照路径
- 外部原因:内存被应用程序使用的太多,而且用完后没有释放,浪费了内存。这种情况下会造成
内存泄漏or内存溢出:内存泄漏:应用进程申请并使用完的内存,没有被释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为当前的申请者不用了,但又不能被JVM虚拟机分配给其他申请者。内存溢出:申请的内存超出了JVM虚拟机能提供的内存大小。
JVM内存模型
这部分内容来自我的上一篇文章:[JVM|内存模型] Java虚拟机的内存模型?也就这7个而已
Java虚拟机所管理的内存包括以下 7个 运行时数据区域:
程序计数器(Program Counter Register)
- 一块较小的内存空间,可以
看作是当前线程所执行的字节码的行号指示器线程私有的内存- 值得注意的是:《Java虚拟机规范》中,
唯一一个没有规定任何OutOfMemoryError情况的区域!!!
Java虚拟机栈(VM Stack)
Java方法执行的线程内存模型- 为虚拟机执行Java方法(也就是
字节码)服务线程私有的内存- 其
生命周期与线程相同- 每个Java方法的执行对应着一个栈帧的进栈和出栈的操作
- 两类异常:
- 如果
线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常- 如果
JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常
本地方法栈(Native Method Stacks)
- 区别于 “Java虚拟机栈” :
本地方法栈只为虚拟机使用到的本地(Native)方法服务,为其运行提供内存环境- 同 “Java虚拟机栈” 一样,
本地方法栈也有两类异常:
栈深度溢出时,将抛出StackOverflowError异常栈扩展失败时,会抛出OutOfMemoryError异常
Java堆(Java Heap)
虚拟机所管理的内存中最大的一块Java堆是被所有线程共享的一块内存区域- 唯一的目的:
存放对象示例。
- Java中 “几乎” 所有的对象实例都在这里分配内存;
- 但是,由于现在技术发展,说 “Java对象示例都分配在堆上” 也渐渐变得不是那么绝对了。
Java堆是垃圾收集器管理的内存区域,也称“GC堆”- Java堆可以处于物理上不连续的内存空间,但
在逻辑上它应该是被视为连续的。- 如果在
Java堆中没有内存完成实例分配,并且Java堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
方法区(Method Area)
- 和 “Java堆” 一样,是
被所有线程共享的一块区域。- 在《Java虚拟机规范》中,把
方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作 “非堆” ,目的是与Java堆区分开来。- 如果
方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
运行时常量池(Running Constant Pool)
运行时常量池是方法区的一部分- 常量池表:用于存放
编译期生成的各种字面量与字符引用。
- 这部分内容将在
类加载后存放到方法区的运行时常量池中。运行时常量池相对Class文件常量池的一个重要特征是具备动态性。- 当
常量池无法再申请到内存时,会抛出OutOfMemoryError异常
直接内存(Direct Memory)
既不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
但是这部分内存区域也被频繁地使用,而且也可能导致
OutOfMemoryError异常出现
- 在
JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。- 在本机直接内存的分配不会受到
Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
OOM的error类型
java.lang.OutOfMemoryError: Java heap space
Java堆 内存溢出,是最常见的一种情况。原因:
- 一般由于
内存泄露或者堆的大小设置不当引起。解决:
对于
内存泄露:需要通过内存监控软件,查找程序中的泄露代码,对于
堆大小,可以通过虚拟机VM参数进行修改:
-Xms1024M -Xmx2048M
java.lang.OutOfMemoryError: PermGen space
方法区溢出
PermGen space的全称是Permanent Generation space(指内存的永久保存区域)。原因:
- 加载了大量的Class(类)
- 在单一的Tomcat实例下运行多个Web应用程序(大量色jsp页面)
- 在运行的Tomcat实例中反复“热部署”Web应用程序
- 采用
cglib等反射机制- 过多的常量也会导致方法区溢出,尤其是字符串
解决:
- 修改
方法区的大小(缺省默认为64M):
-XX:PermSize=128M -XX:MaxPermSize=256M
java.lang.StackOverflowError
- 不会抛出
OOM Error,但是也是比较常见的Java内存溢出情况。Java虚拟机栈or本地方法栈,在栈深度溢出(线程请求的栈深度大于虚拟机所允许的深度),将抛出StackOverflowError异常原因:
- 最常见的:无限递归循环调用(死循环)
- 栈深度溢出
- 执行了大量方法,导致线程栈空间耗尽
- 方法内声明了大量的局部变量
解决:
- 通过程序抛出的异常堆栈,利用内存监控软件,查找程序中执行死循环的代码
- 排查是否存在类之间的循环依赖
- 设置JVM启动参数
-Xss,增加线程栈内存空间
- 线程栈的默认大小依赖于操作系统、JVM 版本和供应商
OOM分析
Heap Dump(堆转储文件)是一个Java进程在某个时间点上的内存快照。
Heap Dump是有着多种类型的。
不过总体上Heap Dump在触发快照的时候都保存了Java对象和类的信息。
-
通常在写
Heap Dump文件前会触发一次FullGC,所以Heap Dump文件中保存的是FullGC后留下的对象信息。 -
配置参数:
-XX:+HeapDumpOnOutOfMemoryError,可以在发生OutOfMemoryError后获取到一份HPROF二进制Heap Dump文件,生成的文件会直接写入到工作目录。
注意:该方法需要
JDK5以上版本。
转存堆内存信息后,需要对文件进行分析,可以使用以下工具,从而找到OOM的原因:
-
JProfiler:IDEA继承了对应插件,详细参考 《Dump分析实战》
-
MAT(Memory Analyzer Tool):基于Eclipse RCP的内存分析工具。具体使用参考:www.eclipse.org/mat/
参考资料
[1] 某厂Java一面:一道JVM面试题引发的“栈帧”血案
[2] 还不会JVM,是准备家里蹲吗?