SpringBoot堆外内存溢出故障排查

405

一、背景

生产环境内存使用异常,最后定位到是频繁大文件访问,而文件访问那里,为了加快访问速度,使用FileChannel和MappedByteBuffer申请了堆外内存,mmap占用的内存映射不会被GC回收。

(基于JDK11)

二、内存组成

堆内存

​ 简单来说,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

  1. 新生代(eden_space)

  2. 逃逸区(survivor_space)

  3. 老生代(old_gen)

“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。

堆外内存

  1. 元空间(方法区):类信息、常量、静态变量、运行时常量池等
  2. 直接内存:NIO MappedByteBuffer DirectByteBuffer MappedByteBufferR
  3. 内存映射(mmap)

线程私有

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈

线程共享

  1. 方法区
  2. 直接内存 (非运行时数据区的一部分)

三、常用调优参数

使用G1垃圾回收,就不要设置新生代内存限制了。

# jdk11默认使用G1垃圾回收
-XX:+UseG1GC

# 删除新生代内存限制,G1垃圾回收会动态调整新生代、老年代内存区域大小,获得最佳时延效果
-Xmn768m
# 最小堆内存,官方推荐为物理内存的1/64
-Xms512m

# 最大堆内存,官方推荐为物理内存的1/4
-Xmx2048m

# 新生代内存,官方推荐为最大堆内存的3/8
-Xmn768m

# 元空间(方法区)
-XX:MaxMetaspaceSize=512m

# 最大直接内存,防止堆外直接内存占满系统可用内存
-XX:MaxDirectMemorySize=512m

# 内存溢出时输出heap dump文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=你要输出的日志路径

# 跟踪内存占用,可以看到堆外内存占用情况,但是无法看到第三方jni native库申请占用的内存,如MappedByteBuffer
-XX:NativeMemoryTracking=summary

四、故障排查手段

jvm

# 查看统计
jstat -gc <PID>

# 查看内存占用
jcmd <PID> VM.native_memory

# 手动GC
jcmd <PID> GC.run

# dump出文件
jmap -dump:format=b,file=heap20220923.hprof <PID>

# 查看堆内对象的分布 Top 50(定位内存泄漏)
jmap –histo:live <PID> | sort-n -r -k2 | head-n 50

visualvm

# 查询堆转储文件的总体情况
GUI:Summary

# 查询size占用比较多的数据类型,大致定位是哪个接口出了问题
GUI: Classes By Number Of Instances

# 基本数据类型byte[],需要自行进行二进制转换显示为可读字符串
GUI: Class By Size Of Instances->Preview

arthas

# 启动
sh /data/software/arthas/as.sh

# 统计,查看Minor GC和Full GC次数
dashboard

# 内存
memory

# 查看top 3进程
thread -n 3

# 查看类源代码
jad com.jhit.CLASS_NAME

# 查看函数源代码
jad com.jhit.CLASS_NAME.FUNCTION_NAME

pmap

# 查看物理内存占用
pmap -x 1497364 | sort -n -k3 > pmap-sorted.txt

smaps

# 查看进程使用内存块信息,找到有问题的内存块地址
cat /proc/1497364/smaps > smaps.txt

gdb

# 附着进程
gdb attach 1497364

# dump出内存块地址的转储文件
dump memory /tmp/0xfffbfc000000-0xfffc00000000.dump 0xfffbfc000000 0xfffc00000000.dump

# 转为可读的字符串文本
strings -10 /tmp/0xfffbfc000000-0xfffc00000000.dump > /tmp/0xfffbfc000000-0xfffc00000000.txt

perf

# 略

五、相关知识

连接数查询

# 所有连接数
netstat | wc -l

# 活跃连接数
netstat -an | grep ESTABLISHED | wc -l

# 另一种方法
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 某个端口的活跃连接数(存在问题)
netstat -an | grep -i '7956' | grep ESTABLISHED | wc -l

CPU占用排查

# 内存
ps axo %mem,pid,euser,cmd | sort -nr | head -10

# CPU
ps -aeo pcpu,user,pid,cmd | sort -nr | head -10

按线程状态统计线程数

jstack 2759362 | grep java.lang.Thread.State:|sort|uniq -c | awk '{sum+=$1; split($0,a,":");gsub(/^[ \t]+|[ \t]+$/, "", a[2]);printf "%s: %s\n", a[2], $1}; END {printf "TOTAL: %s",sum}';