SpringBoot堆外内存溢出故障排查
一、背景
生产环境内存使用异常,最后定位到是频繁大文件访问,而文件访问那里,为了加快访问速度,使用FileChannel和MappedByteBuffer申请了堆外内存,mmap占用的内存映射不会被GC回收。
(基于JDK11)
二、内存组成
堆内存
简单来说,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
-
新生代(eden_space)
-
逃逸区(survivor_space)
-
老生代(old_gen)
“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。
堆外内存
- 元空间(方法区):类信息、常量、静态变量、运行时常量池等
- 直接内存:NIO MappedByteBuffer DirectByteBuffer MappedByteBufferR
- 内存映射(mmap)
线程私有
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
三、常用调优参数
使用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}';