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 时各个空间垃圾回收情况,联合分析定位原因。

results matching ""

    No results matching ""