一、Java的内存模型
《Java虚拟机规范》规定虚拟机包括以下几个运行时数据区:
- 方法区 - Method Area
- 虚拟机栈 - VM Stack
- 本地方法栈 - Native Method Stack
- 堆 - Heap
- 程序计数器 - Program Counter Register
在不同的JVM
运行时实现中可能存在差异
常见的JVM
运行时有:
- OracleJDK
- OpenJDK
- IBM J9
- JRocket
- Alibaba Dragonwell
- Tencent Kona
- Graalvm
比如OracleJDK8
的方法区区叫元空间,具体不再展开...
不考虑差异性,本文主旨是线上OracleJDK8
说明内存溢出出现的场景,排查方式和解决方案
二、内存溢出类型
- StackOverflowError
- OutOfMemoryError
StackOverflowError
当线程栈深度超过设置的最大深度,则由虚拟机抛出 OutOfMemoryError
当无法通过JVM申请到内存时,则由虚拟机抛出
除了程序计数器外,栈、堆、非堆(直接内存)都可以抛出OOM
常见的异常错误及通用解:
- java.lang.StackOverflowError : Thread Stack space
原因:当线程栈深度超过设置的最大深度,则由虚拟机抛出
解决:
1)使用循环替换递归
2)使用调度器MapReduce思想来实现分之归并
- java.lang.OutOfMemoryError: Java heap space
原因:当前线程无法从JVM申请堆内存空间
解决:
1)具体问题具体分析,当定位到是大对象时,需要优化代码为小对象
2)当定位到是堆内存空间太小,修改VM配置。这里需评估并发量,经过压测和优化后得出
3)优化代码,减少处理耗时,如果是高耗时操作,可以在对象不使用后,去掉局部变量对大对象的引用
4)当定位到是内存泄露,则需要修改泄露代码(通常在引用第三方包出现)
- java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因:申请数组大小超过JVM允许值,不同JVM版本存在差异
解决:
1) 修改申请数组小大
- java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
原因:当 GC 为释放很小空间占用大量时间时抛出,通常会伴随着CPU100%异常,线上系统严重卡顿
解决:跟Java heap space
处理方法一样
- java.lang.OutOfMemoryError: Metaspace
原因:当前线程无法从JVM申请元空间内存,JDK8之后原永久代中的类元数据迁移到元空间(堆 -> 直接内存),常量池继续在堆中
解决:
1)当定位到是元空间设置太小,修改配置
2)当定位到是内存泄露,则修改代码(通常在引用第三方包出现)
- java.lang.OutOfMemoryError: Direct buffer memory
原因:一般是工具为了做零拷贝,直接将数据存储在直接内存中,减少用户态和内核态内存拷贝,当未做内存回收时,空间不释放,抛出异常
解决:
1)定位到直接内存设置太小,修改配置
2)定位到内存泄露未释放内存,则修改异常代码
3)定位到Metaspace
未设置大小,该空间内存泄露,则修改异常代码
疑难借助工具排查:
- VisualVM
- Eclipse MAT(Memory Analyzer Tool)
- Alibab Arthas