什么是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,是准备家里蹲吗?