1. JVM 内存溢出 OOM
OOM(java.lang.OutOfMemoryError)是 JVM 在无法分配足够内存时抛出的错误,通常由以下原因导致:
- 方法区(元空间)内存溢出(Metaspace)
- 栈内存溢出(StackOverflowError)
- 堆内存溢出(Heap Space)
- 直接内存(堆外内存)溢出(Direct Memory)
1.1. 各种类型 OOM 原因及排查方法
回顾一下之前总结中,JVM 的内存区域,分为方法区(元空间)、栈内存、堆内存,直接内存(堆外内存)是一些特殊情况会使用的区域。
1.1.1. 方法区(元空间)内存溢出
方法区(元空间)是用来存储类的区域。会报错java.lang.OutOfMemoryError: Metaspace
。
内存溢出原因
- JVM 设置的元空间内存太小(
-XX:MaxMetaspaceSize
太小) - 类加载过多(大部分原因是动态生成类,导致生成过多,内存溢出)
1.1.2. 栈内存溢出
栈内存是用来存储每个线程运行是相关方法数据及局部变量的区域。会报错java.lang.StackOverflowError
。
- JVM 栈内存设置的太小(
-Xss
太小) - 线程调用的方法太多 或者 递归调用过深,死循环等
1.1.3. 堆内存溢出
堆内存是程序执行的时候,创建的对象存放的区域。会报错java.lang.OutOfMemoryError: Java heap space
。
内存溢出原因
- 超大对象数组等,堆内存放不下
- 系统访问量激增,导致设置的内存不足,新创建对象无法分配
- 内存泄露(程序中有不断新创建对象的逻辑,一直被引用无法回收)
1.1.4. 直接内存(堆外内存)溢出
直接内存(堆外内存)一般是 NIO 使用的空间区域。会报错java.lang.OutOfMemoryError: Direct buffer memory
。
内存溢出原因
-XX:MaxDirectMemorySize
设置太小- 分配的直接内存,被耗尽
1.2. OOM 排查原因及解决办法
日常项目中,可以配置监控系统,用来监控机器相关资源及机器上 jvm 的内存使用情况等等。当超过限制可以发起报警。
1.2.1. 设置打印日志及保留内存快照
服务在启动的时候,可以设置打印 gc Log 日志,方便追踪查找问题,也可以设置 OOM 时生成 dump 内存快照,这样方便定位对象分布情况,更快速的定位 OOM 原因。
保留 GC Log 可以在程序启动时设置以下参数,-XX:+PrintGCDetails
参数代表打印详细的gc日志;-XX:+PrintGCTimeStamps
参数可以打印出来每次GC发生的时间;-Xloggc:gc.log
参数可以设置将gc日志写入一个磁盘文件。
保留 OOM 内存快照,可以在程序启动时设置以下参数,-XX:+HeapDumpOnOutOfMemoryError
参数意思是在 OOM 的时候自动 dump 内存快照;-XX:HeapDumpPath=/usr/local/app/oom
参数是说把内存快照放到什么地方去。
1.2.2. 动态实时观测内存相关变化
jstat 查看系统 GC 情况
检查JVM的整体运行情况,比较实用的工具之一,就是jstat。它可以看到当前运行JVM内的Eden、Survivor、老年代的内存使用情况,还有Young GC和Full gC的执行次数以及耗时。
使用命令jstat -gc PID
,然后就可以看到对应 Java进程(本质是一个JVM)的内存和GC情况。
jstat常用命令:
jstat -gccapacity PID:堆内存分析
jstat -gcnew PID:年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity PID:年轻代内存分析
jstat -gcold PID:老年代GC分析
jstat -gcoldcapacity PID:老年代内存分析
jstat -gcmetacapacity PID:元数据区内存分析
jstat -gc PID 1000 10:每隔1秒更新出来最新的一行jstat统计信息,一共执行10次jstat统计
使用命令后打印各个参数含义:
S0C:这是From Survivor区的大小
S1C:这是To Survivor区的大小
S0U:这是From Survivor区当前使用的内存大小
S1U:这是To Survivor区当前使用的内存大小
EC:这是Eden区的大小
EU:这是Eden区当前使用的内存大小
OC:这是老年代的大小
OU:这是老年代当前使用的内存大小
MC:这是方法区(永久代、元数据区)的大小
MU:这是方法区(永久代、元数据区)的当前使用的内存大小
YGC:这是系统运行迄今为止的Young GC次数
YGCT:这是Young GC的耗时
FGC:这是系统运行迄今为止的Full GC次数
FGCT:这是Full GC的耗时
GCT:这是所有GC的总耗时
jmap 和 jhat 查看系统对象分布情况
有的时候会发现JVM新增对象的速度很快,如果想看什么对象占据了大量内存。可以使用命令jmap -heap PID
,会打印堆内存相关参数设置,及当前堆内存各个区域的情况(jstat打印也可以看到)。
如果知道各种对象占用内存空间的大小并降序排列,可以使用命令jmap -histo PID
,结果会把占用内存最多的对象放在最上面。
jmap命令可以生成堆内存快照放到一个文件中,使用命令jmap -dump:live,format=b,file=dump.hprof PID
。
然后使用命令jhat -port 7000 dump.hprof
可以启动jhat服务器(可以指定其他端口号,默认是7000端口号),就可以在浏览器上访问当前这台机器的7000端口号,通过图形化的方式去分析堆内存里的对象分布情况。
1.2.3. 日常系统 OOM 问题定位
通过项目日志、 gc Log 和内存快照等联合判断 OOM 问题。日志中可以分别 OOM 类型(参看文章上面报错)。
如果是栈内存溢出,找到对应代码位置,判断是否有死循环或者递归调用过深等情况。
如果是元空间及堆内存溢出,结合对象快照,查看对象分布及 GC 时各个空间垃圾回收情况,联合分析定位原因。